Book a free strategy call — pick a time that works for you Book Now →
Gmail Pub/Sub setup with OpenClaw on VPS

Gmail Pub/Sub on a VPS: Fix the Org Policy Blocker That Stops Every Google Workspace Setup

Every Google Workspace org policy blocks the same service account. You run the gcloud pubsub topics add-iam-policy-binding command, it fails with iam.allowedPolicyMemberDomains, and you spend the next three hours searching the wrong admin console. We have deployed Gmail Pub/Sub on bare-metal VPS servers for multiple OpenClaw Google Workspace installations through ManageMyClaw, and the org policy blocker is the single issue that stops every first-time setup cold.

Gmail Pub/Sub delivers email notifications in 30 seconds flat. The org policy error that blocks it takes 3 hours to diagnose — not because the fix is hard, but because Google buries it in the wrong console.

OpenClaw is an open-source AI agent framework that automates Gmail, Calendar, and other Google Workspace services on your own VPS. Gog is the CLI tool that handles OpenClaw’s Google integration — OAuth, Gmail watch, webhook serving, and Pub/Sub plumbing. ManageMyClaw deploys and maintains these systems so you don’t debug org policies at 2 AM. Three products, one pipeline, and a single IAM binding standing between you and real-time email processing.

This guide walks through the complete Gmail Pub/Sub setup on a bare-metal VPS, from project creation to end-to-end verification. The org policy fix is in Step 3a — but if you skip ahead, you’ll miss the context that makes the fix make sense.

Architecture

How Gmail Pub/Sub Delivers Real-Time Notifications

The pipeline has seven handoffs, and every one must work for a single notification to reach your OpenClaw agent. A new email arrives at your Google Workspace inbox. Gmail’s Watch API detects the change and publishes a notification to your Google Cloud Pub/Sub topic. Pub/Sub pushes a POST request to your HTTPS webhook endpoint. Nginx terminates SSL and proxies the request to gog serve running on localhost. Gog fetches the full email content via the Gmail API. Your OpenClaw agent processes the email — categorizes it, drafts a reply, fires a notification.

~30s End-to-end latency from inbox arrival to agent processing
Component Purpose
Google Cloud Project Houses Pub/Sub topic, OAuth credentials, API access
Gmail API Email access and watch registration
Cloud Pub/Sub API Message delivery from Gmail to your server
OAuth 2.0 Client (Desktop) Headless auth for your VPS-based OpenClaw agent
HTTPS Webhook Endpoint Receives Pub/Sub push notifications via nginx
Gog CLI Processes notifications, fetches email, forwards to OpenClaw
SSL Certificate Required — Pub/Sub only pushes to HTTPS endpoints

Seven components to receive a push notification that polling handles with a single cron line. The trade-off is latency: 30 seconds vs. up to 5 minutes.

Prerequisites

What You Need Before Starting

Prerequisites Checklist

  • Google Workspace account (e.g., support@yourdomain.com) — personal Gmail accounts work but lack org policy controls
  • A VPS with a public IP and a domain pointing to it — bare-metal or cloud VM with systemd
  • Nginx installed with SSL via Let’s Encrypt — Pub/Sub will not deliver to HTTP endpoints
  • gcloud CLI installed on your local machine — for GCP project management and org policy overrides
  • Gog CLI installed on the VPSOAuth must be configured first
  • OpenClaw agent running on the VPS with a hook endpoint for incoming email events
Step 1

Create a Google Cloud Project and Enable APIs

Start at the Google Cloud Console. Create a new project (e.g., my-email-notifications) and note the Project ID — you’ll reference it in every command from here forward.

$ gcloud services enable gmail.googleapis.com pubsub.googleapis.com
  –project=my-email-notifications
✓ Gmail API enabled
✓ Cloud Pub/Sub API enabled

If your OpenClaw agent also handles Calendar, Drive, or Contacts, enable those APIs now too:

$ gcloud services enable calendar-json.googleapis.com
  drive.googleapis.com people.googleapis.com
  tasks.googleapis.com –project=my-email-notifications
✓ Additional APIs enabled
APIs vs. OAuth scopes

Enabling an API and granting an OAuth scope are two different things. Having the Gmail scope in your OAuth consent screen does not mean the Gmail API is active in your GCP project. You need both. This distinction trips up even experienced developers.

Step 2

Set Up OAuth 2.0 Credentials

Create an OAuth consent screen set to “Internal” — this skips Google’s app verification process entirely, which only applies to external apps. Add all required scopes for Gmail, Calendar, Drive, and any other services your OpenClaw agent will use.

  1. Go to APIs & Services > OAuth consent screen. Select Internal. Set app name, support email, and developer contact.
  2. Go to APIs & Services > Credentials. Click Create Credentials > OAuth client ID. Type: Desktop app. Name it something like OpenClaw VPS Watcher.
  3. Download the client_secret.json file and upload it to your VPS:
$ scp client_secret.json user@your-vps:/opt/openclaw/google-client-secret.json
✓ Credentials uploaded

Desktop app type, not web app. This matters for the headless OAuth flow you’ll run on the VPS in Step 6.

Step 3

Create the Pub/Sub Topic and Grant Gmail Permission

This is where most setups break. Creating the topic is straightforward. Granting Gmail permission to publish to it is not.

$ gcloud pubsub topics create gmail-inbox-watch
  –project=my-email-notifications
✓ Topic created

Now grant Gmail’s internal service account permission to publish messages to your topic:

$ gcloud pubsub topics add-iam-policy-binding
  projects/my-email-notifications/topics/gmail-inbox-watch
  –member=’serviceAccount:gmail-api-push@system.gserviceaccount.com’
  –role=’roles/pubsub.publisher’

If this command succeeds, skip to Step 4. If it fails with the iam.allowedPolicyMemberDomains error, you’ve hit the org policy blocker. Keep reading.

Step 3a

Fixing the Org Policy Blocker — The Error That Stops Every Setup

The Error Output FAILED_PRECONDITION
FAILED_PRECONDITION: One or more users named in the policy
do not belong to a permitted customer.
  – description: User gmail-api-push@system.gserviceaccount.com
    is not in permitted organization.
  – type: constraints/iam.allowedPolicyMemberDomains

What’s happening: Google Workspace organizations have a default policy called Domain Restricted Sharing (iam.allowedPolicyMemberDomains). This policy prevents adding IAM members from outside your organization. The problem is that gmail-api-push@system.gserviceaccount.com is a Google-owned system service account. It’s not part of your org, so the policy blocks it — even though Gmail Pub/Sub requires it.

The #1 mistake: wrong console

This is configured in the Google Cloud Console (console.cloud.google.com), NOT the Google Workspace Admin Console (admin.google.com). The Workspace Admin Console has a “Google Cloud session control” page that looks related but controls a completely different setting. This distinction costs people hours.

Option A: Override via gcloud CLI (Recommended)

First, enable the Organization Policy API:

$ gcloud services enable orgpolicy.googleapis.com
  –project=my-email-notifications
✓ Organization Policy API enabled

Then override the policy at the project level:

$ gcloud org-policies set-policy /dev/stdin
  –project=my-email-notifications <<‘EOF’
name: projects/my-email-notifications/policies/iam.allowedPolicyMemberDomains
spec:
  rules:
    – allowAll: true
EOF
✓ Policy overridden at project level

Option B: Override via GCP Console UI

  1. Navigate to: console.cloud.google.com/iam-admin/orgpolicies/iam.allowedPolicyMemberDomains?project=my-email-notifications
  2. Click “MANAGE POLICY” and select “Override parent’s policy”
  3. Add a rule set to “Allow All” and click Save
Required IAM role

Your account needs the Organization Policy Administrator role at the organization level. If you don’t have it, an existing admin must grant it:

$ gcloud organizations list
DISPLAY_NAME  ID            DIRECTORY_CUSTOMER_ID
yourdomain    123456789    C0xxxxxxx
$ gcloud organizations add-iam-policy-binding 123456789
  –member=’user:admin@yourdomain.com’
  –role=’roles/orgpolicy.policyAdmin’
✓ Role granted

Now Retry the Permission Grant

$ gcloud pubsub topics add-iam-policy-binding
  projects/my-email-notifications/topics/gmail-inbox-watch
  –member=’serviceAccount:gmail-api-push@system.gserviceaccount.com’
  –role=’roles/pubsub.publisher’
✓ Updated IAM policy for topic [gmail-inbox-watch]
bindings:
– members:
  – serviceAccount:gmail-api-push@system.gserviceaccount.com
  role: roles/pubsub.publisher
Security: re-tighten after granting

After the IAM binding succeeds, re-tighten the org policy immediately. Existing bindings are NOT retroactively removed — the policy only checks at binding creation time. Run: gcloud org-policies reset iam.allowedPolicyMemberDomains --project=my-email-notifications

Step 4

Create the Push Subscription

$ gcloud pubsub subscriptions create gmail-inbox-push
  –topic=projects/my-email-notifications/topics/gmail-inbox-watch
  –push-endpoint=”https://webhook.yourdomain.com/gmail-pubsub”
  –ack-deadline=30
  –project=my-email-notifications
✓ Subscription created

Four requirements for the push endpoint: must be HTTPS (not HTTP), must have a valid SSL certificate (Let’s Encrypt works), must be publicly accessible from the internet, and must respond with a 2xx status code to acknowledge messages.

Step 5

Configure Nginx as a Reverse Proxy

Gog runs on localhost (port 8788 by default). Nginx terminates SSL and proxies Pub/Sub push notifications to it.

# /etc/nginx/sites-available/webhook
server {
  listen 80;
  server_name webhook.yourdomain.com;
  return 301 https://$host$request_uri;
}
server {
  listen 443 ssl http2;
  server_name webhook.yourdomain.com;
  ssl_certificate /etc/letsencrypt/live/webhook.yourdomain.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/webhook.yourdomain.com/privkey.pem;
  add_header X-Frame-Options DENY always;
  add_header X-Content-Type-Options nosniff always;
  location /gmail-pubsub {
    proxy_pass http://127.0.0.1:8788;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 60s;
    client_max_body_size 1m;
  }
  location /health {
    return 200 ‘OK’;
    add_header Content-Type text/plain;
  }
  location / {
    return 403 ‘Forbidden’;
  }
}

Get the SSL certificate and enable the site:

$ certbot certonly –webroot -w /var/www/html
  -d webhook.yourdomain.com
  –non-interactive –agree-tos –email admin@yourdomain.com
✓ Certificate obtained
$ ln -sf /etc/nginx/sites-available/webhook /etc/nginx/sites-enabled/
$ nginx -t && systemctl reload nginx
✓ Nginx reloaded
Step 6

Authenticate OAuth on the VPS (Headless Flow)

Your VPS has no browser. Gog uses a 2-step remote auth flow — generate the URL on the VPS, authorize in your laptop’s browser, paste the callback URL back.

Generate the Auth URL (on VPS)

$ gog login user@yourdomain.com
  –services=all –remote –step=1
  –force-consent –no-input
auth_url: https://accounts.google.com/o/oauth2/v2/auth?client_id=…

Authorize in Browser (on your laptop)

Open the auth URL in your browser. Sign in with the correct Google account. Grant all requested permissions. The browser redirects to http://127.0.0.1:XXXXX/oauth2/callback?... and shows a “can’t connect” error. This is expected behavior. Copy the full URL from the address bar.

Exchange the Code (on VPS)

$ gog login user@yourdomain.com
  –services=all –remote –step=2
  –force-consent –no-input
  –auth-url ‘http://127.0.0.1:XXXXX/oauth2/callback?state=…&code=…&scope=…’
✓ OAuth tokens saved
Common OAuth mistakes

URL breaks across lines when copying from SSH — widen your terminal window first. Auth codes are time-sensitive — complete the exchange within 5 minutes. And always use --force-consent when re-authenticating to ensure a fresh refresh token.

The “can’t connect” page in your browser is the part that panics every first-timer. It’s normal. The auth code is embedded in the URL parameters — you don’t need the page to actually load.

Step 7

Start the Gmail Watch

Register a watch on the inbox. This tells Gmail to publish notifications to your Pub/Sub topic whenever a new email arrives.

$ gog gmail watch start
  –label INBOX
  –topic projects/my-email-notifications/topics/gmail-inbox-watch
  –account user@yourdomain.com
  –no-input
account     user@yourdomain.com
topic       projects/my-email-notifications/topics/gmail-inbox-watch
labels      INBOX
history_id  1056216
expiration  2026-04-05T14:21:02Z
7 days Gmail watch expiration — no notification, no grace period, silent failure

That expiration date is not a suggestion. When it passes, Gmail silently stops publishing. No error in your logs. No alert. Your agent just stops receiving email notifications and nobody knows until someone asks why the urgent email from 3 hours ago wasn’t processed.

Step 8

Set Up the Webhook Server as a Systemd Service

Run gog gmail watch serve as a systemd service so it survives reboots and auto-restarts on failure.

# /etc/systemd/system/gmail-webhook.service
[Unit]
Description=Gmail Webhook Server
After=network.target
[Service]
Type=simple
User=root
Environment=GOG_KEYRING_PASSWORD=your-password-here
Environment=GOG_ACCOUNT=user@yourdomain.com
Environment=GOG_KEYRING_BACKEND=file
ExecStart=/usr/local/bin/gog gmail watch serve
  –bind 127.0.0.1 –port 8788
  –path /gmail-pubsub
  –include-body –max-bytes 20000
  –hook-url http://127.0.0.1:YOUR_OPENCLAW_PORT/hooks/gmail
  –hook-token YOUR_HOOK_TOKEN
  –no-input
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
Order matters: watch before serve

Start the Gmail watch (Step 7) BEFORE starting this service. The service will crash with watch state not found if the watch hasn’t been registered yet. This creates a restart loop that systemd will eventually give up on.

$ systemctl enable gmail-webhook.service
$ systemctl start gmail-webhook.service
$ systemctl status gmail-webhook.service
✓ Active (running)
$ ss -tlnp | grep 8788
LISTEN 0 128 127.0.0.1:8788 0.0.0.0:*
Step 9

Set Up Watch Renewal Cron

Gmail watch expires every 7 days with zero warning. Create a daily renewal script that runs before the watch can expire. For more on cron-based email automation with OpenClaw, see our dedicated guide.

$ cat > /usr/local/bin/gmail-watch-renew.sh << ‘EOF’
#!/bin/bash
export GOG_KEYRING_PASSWORD=your-password-here
export GOG_ACCOUNT=user@yourdomain.com
export GOG_KEYRING_BACKEND=file
/usr/local/bin/gog gmail watch start
  –label INBOX
  –topic projects/my-email-notifications/topics/gmail-inbox-watch
  >> /var/log/gmail-watch-renew.log 2>&1
EOF
$ chmod +x /usr/local/bin/gmail-watch-renew.sh
✓ Renewal script created
$ (crontab -l 2>/dev/null; echo ‘0 3 * * * /usr/local/bin/gmail-watch-renew.sh’) | crontab –
✓ Daily cron at 3 AM UTC
Step 10

Verify the Full Pipeline

Check each component individually before running the end-to-end test.

# 1. Webhook endpoint reachable?
$ curl -sI https://webhook.yourdomain.com/health
HTTP/2 200
# 2. Webhook server listening?
$ ss -tlnp | grep 8788
LISTEN 0 128 127.0.0.1:8788 0.0.0.0:*
# 3. Gmail watch active?
$ gog gmail watch status –account user@yourdomain.com
expiration: 2026-04-05T14:21:02Z (6 days remaining)
# 4. Pub/Sub subscription active?
$ gcloud pubsub subscriptions describe gmail-inbox-push
  –project=my-email-notifications
pushConfig: endpoint: https://webhook.yourdomain.com/gmail-pubsub

End-to-end test: Send a test email to user@yourdomain.com from a different account. Watch your OpenClaw agent’s logs. You should see the notification arrive within approximately 30 seconds.

Best Practices

Do’s and Don’ts

Do

Use --services=all when authenticating to avoid re-doing OAuth later. Set up watch renewal cron — Gmail watch expires every 7 days. Start Gmail watch BEFORE starting the webhook service. Use a dedicated webhook subdomain. Use systemd for auto-restart. Keep your OAuth consent screen as “Internal.” Use --force-consent when re-authenticating. Re-tighten the org policy after granting the Pub/Sub permission.

Don’t
  • Fix iam.allowedPolicyMemberDomains in the Workspace Admin Console — it’s in the GCP Console
  • Use HTTP for the push endpoint — Pub/Sub requires HTTPS
  • Forget to enable APIs in the GCP project — OAuth scopes alone aren’t enough
  • Use gcloud auth login interactively on a headless VPS — use --no-browser
  • Rely solely on polling when Pub/Sub is available — polling wastes API calls and adds latency
  • Hardcode credentials in application code — use environment variables or keyring
Fallback

When You Cannot Change the Org Policy

If corporate IT controls the org policy and won’t budge, use heartbeat polling as a fallback. It works. It adds 0-5 minutes of latency. It uses more API quota. But it doesn’t require any GCP configuration beyond OAuth.

# Cron job: check inbox every 5 minutes
$ */5 * * * * /usr/local/bin/gog gmail search “in:inbox” –max 10 –json
  >> /var/log/inbox-check.log 2>&1

Not elegant. Not real-time. But it works every time, on every Google Workspace setup, without touching a single org policy.

Bottom Line

The Bottom Line

Gmail Pub/Sub is the correct architecture for real-time email processing on a VPS. The iam.allowedPolicyMemberDomains org policy blocker is the single obstacle that stops every Google Workspace setup, and it’s fixable in under 5 minutes once you know the fix is in the GCP Console, not the Workspace Admin Console. The confusion between the two consoles is what turns a 5-minute fix into a 3-hour debugging session.

The setup is not trivial — a Google Cloud project, a Pub/Sub topic, an IAM binding, a push subscription, nginx with SSL, a systemd service, and a daily cron job that must never fail. But every component serves a purpose, and once the pipeline is running, your OpenClaw agent processes email in 30 seconds instead of 5 minutes. For security-conscious deployments, the reduced API surface of event-driven processing is a meaningful improvement over constant polling.

If you can change the org policy: use Pub/Sub with heartbeat polling as a fallback. If you can’t: polling alone is perfectly fine for most email triage use cases. Either way, your OpenClaw agent processes email. The architecture is a latency decision, not a capability decision.

FAQ

Frequently Asked Questions

Why does the iam.allowedPolicyMemberDomains error happen?

Google Workspace organizations have a default policy called Domain Restricted Sharing that prevents adding IAM members from outside your organization. The gmail-api-push@system.gserviceaccount.com service account is owned by Google, not your org, so the policy blocks it — even though Gmail Pub/Sub requires this exact binding to function. The fix is overriding the policy at the project level in the GCP Console (not the Workspace Admin Console), granting the binding, then re-tightening the policy.

Can I fix this in the Google Workspace Admin Console?

No. The iam.allowedPolicyMemberDomains constraint is a Google Cloud Organization Policy, managed exclusively through the GCP Console at console.cloud.google.com. The Workspace Admin Console at admin.google.com has a “Google Cloud session control” page that looks related but controls an entirely different setting. This is the #1 source of wasted debugging time.

Is it safe to set allowAll on the org policy?

Yes, temporarily. Set the override at the project level (not the organization level), grant the gmail-api-push service account its Publisher role, then immediately re-tighten the policy with gcloud org-policies reset. Existing IAM bindings are not retroactively removed — the policy only checks at binding creation time. The window of exposure is seconds, not permanent.

What if I can’t change the org policy?

Use heartbeat polling as a fallback. Set up a cron job that runs gog gmail search every 5 minutes. You lose real-time latency (0-5 minute delay instead of 30 seconds) and use more API quota, but it works on every Google Workspace setup without any GCP configuration beyond OAuth. Many OpenClaw deployments run successfully on polling alone.

Skip the Setup Headaches ManageMyClaw handles Gmail Pub/Sub, OAuth, and every VPS integration. See Pricing

Not affiliated with or endorsed by the OpenClaw open-source project.