TL;DR: I set up instant Gmail notifications to my AI assistant using Google Pub/Sub's streaming pull — no webhooks, no exposed ports, no ngrok. Just an outbound gRPC connection that receives push notifications in real-time.
I wanted my AI assistant (Clawdbot, running on a Raspberry Pi 5) to know when I get important emails. The obvious solutions all had problems:
- Polling Gmail API — Works, but 5-minute latency feels archaic
- Gmail Pub/Sub with push webhooks — Requires exposing a port to the internet. No thanks.
- Tailscale Funnel / ngrok — Still exposes a service. Attack surface I don't want.
Google Pub/Sub supports two subscription types:
- Push — Google calls YOUR webhook (requires exposed port)
- Pull — YOU call Google's API to fetch messages
But here's what most people miss: Pull isn't just polling. Pub/Sub supports streaming pull via gRPC — your client opens an outbound connection and holds it open. Google sends messages through that connection in real-time.
No exposed ports. No inbound connections. Sub-second latency.
This was the key insight: separate credentials with minimal scope.
| Component | Credentials | Permissions |
|---|---|---|
| Pub/Sub daemon | Service account | Can ONLY pull from one subscription |
| Email fetching | OAuth token | Gmail read-only |
| Clawdbot webhook | Localhost only | No external access |
If the Pi is compromised:
- The service account can't read emails (only knows "something arrived")
- The OAuth token can read but not send emails
- Neither can access other GCP resources
# Create Pub/Sub topic
gcloud pubsub topics create gmail-watch
# Allow Gmail to publish
gcloud pubsub topics add-iam-policy-binding gmail-watch \
--member="serviceAccount:gmail-api-push@system.gserviceaccount.com" \
--role="roles/pubsub.publisher"
# Create pull subscription
gcloud pubsub subscriptions create gmail-watch-pull \
--topic=gmail-watch
# Create minimal service account
gcloud iam service-accounts create gmail-pubsub-reader
# Grant ONLY subscriber permission on this ONE subscription
gcloud pubsub subscriptions add-iam-policy-binding gmail-watch-pull \
--member="serviceAccount:gmail-pubsub-reader@PROJECT.iam.gserviceaccount.com" \
--role="roles/pubsub.subscriber"# Set up Gmail watch (requires OAuth token with Gmail scope)
# This tells Gmail to publish notifications to your Pub/Sub topic
curl -X POST \
'https://gmail.googleapis.com/gmail/v1/users/me/watch' \
-H 'Authorization: Bearer YOUR_OAUTH_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"topicName": "projects/PROJECT/topics/gmail-watch",
"labelIds": ["INBOX"]
}'from google.cloud import pubsub_v1
import json
def callback(message):
data = json.loads(message.data.decode())
history_id = data.get("historyId")
# Gmail only sends "something changed" — fetch actual content separately
emails = fetch_emails_via_gmail_api(history_id)
for email in emails:
notify_my_assistant(email)
message.ack()
subscriber = pubsub_v1.SubscriberClient()
subscription_path = subscriber.subscription_path("PROJECT", "gmail-watch-pull")
subscriber.subscribe(subscription_path, callback=callback)
# Keep the main thread alive
import time
while True:
time.sleep(60)The subscribe() call opens a streaming gRPC connection. It blocks forever, calling your callback when messages arrive.
[Unit]
Description=Gmail Pub/Sub Watch Daemon
After=network-online.target
[Service]
Type=simple
ExecStart=/path/to/venv/bin/python /path/to/gmail_watch.py
Restart=always
RestartSec=10
[Install]
WantedBy=default.target- Gmail watch expires after 7 days — Need a cron job to renew
- Pub/Sub may deliver duplicates — Track seen message IDs
- Gmail only sends historyId — You still need to call Gmail API for content
- Multiple notifications per email — Deduplicate by message ID
- Latency: ~2-3 seconds from email arrival to notification
- Security: No exposed ports, minimal credential scope
- Reliability: Systemd restarts on failure, gRPC reconnects automatically
- Cost: Free tier covers personal email easily
The conventional wisdom is "you need a webhook for real-time notifications." But streaming pull gives you the same latency with a fundamentally better security model — your server never accepts inbound connections.
This pattern works for any Pub/Sub use case, not just Gmail. If you're building something that needs real-time events but you don't want to expose a webhook, streaming pull is the answer.
Running on a Raspberry Pi 5 with Clawdbot.