Skip to content

Your own Google Cloud project (Gmail, Calendar, Drive)

IMAP mail is in the alpha (owner, corrected 2026-10-27) and is how Gmail connects (app password); the Gmail-API plugin stays parked until demand. Google Calendar and Drive are separate and unaffected.

Around twenty minutes, plus ten more for Gmail push. At the end, your users connect Gmail, Google Calendar and Drive in TeamHUD through an OAuth client your Google Cloud project owns, and new mail shows up the moment it lands rather than on the next poll.

Read INTEGRATIONS.md first. It covers the hostname requirement and the desktop setting without which none of this is used.

Throughout, https://teamhud.example.com stands for your PUBLIC_BASE_URL and my-project for your Google Cloud project id.

Which account type you have decides how hard this is

Section titled “Which account type you have decides how hard this is”
Your users are on Consent screen Google review Catch
Google Workspace (company domain) Internal None None. This is the path to take
Consumer @gmail.com External, left in Testing None Max 100 named test users, and their tokens expire every 7 days, so everyone reconnects weekly and Gmail push dies with the token
Consumer, and you want no expiry External, published Required Gmail’s scopes are restricted: Google verification plus a paid third-party security assessment (CASA), renewed yearly. Rarely worth it for one team

The rest of this guide assumes Workspace + Internal. Differences for External are called out inline.

https://console.cloud.google.com/ → project picker → New project. Put it under your Workspace organization, not “No organization”. Only a project inside the org can use an Internal consent screen.

APIs & Services → Library, enable each you want:

For Enable
Mail Gmail API
Gmail push (step 6) Cloud Pub/Sub API
Calendar Google Calendar API
Drive Google Drive API

Or from Cloud Shell:

Terminal window
gcloud config set project my-project
gcloud services enable gmail.googleapis.com pubsub.googleapis.com \
calendar-json.googleapis.com drive.googleapis.com

Google Auth Platform → Branding / Audience (older consoles: APIs & Services → OAuth consent screen).

  • Audience / User type: Internal.
  • App name: what your users will see when they approve access. Use your company name in it.
  • Support email, developer contact: your admin address.
  • Authorized domains: add your server’s domain (example.com).
  • Scopes: an Internal app does not have to list them. TeamHUD asks at connect time, and the user sees exactly what they approve.

External only: list the scopes below under Data access, and add every user under Audience → Test users.

Google Auth Platform → Clients → Create client (older: Credentials → Create credentials → OAuth client ID).

  • Application type: Web application. Not “Desktop app”. Your server does the code exchange, on a public HTTPS callback, so the desktop never holds the client secret. (TeamHUD’s separate “use your own Google app” preference does want a Desktop client, but that is a different, per-user feature. It does not apply here.)

  • Authorized redirect URIs: exactly

    https://teamhud.example.com/oauth/google/callback

    Scheme, host, path, no trailing slash. Google compares byte for byte.

  • Authorized JavaScript origins: leave empty.

Copy the Client ID and Client secret.

Terminal window
GOOGLE_CLIENT_ID=1234567890-abc.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-...
# Pin the callback so a later PUBLIC_BASE_URL change cannot silently move it:
GOOGLE_REDIRECT_URI=https://teamhud.example.com/oauth/google/callback
Terminal window
docker compose up -d

At this point Gmail, Calendar and Drive connect and work. On a desktop configured per INTEGRATIONS.md, connect a Google account. The consent screen should show your app name. Mail is fetched by polling until you do step 6.

Each service asks separately, when the user connects it:

Service Scopes Held by
Gmail: reading, archiving, replying gmail.modify, gmail.send the user’s desktop
Gmail push gmail.metadata (sees that mail arrived, never its content) your server, encrypted
Calendar calendar the user’s desktop
Drive drive.readonly the user’s desktop
All of the above openid, userinfo.email, userinfo.profile —

Connecting Gmail therefore shows two consent screens: first the metadata-only grant for push, then the real one. That is deliberate. Your server only ever holds a token that cannot read mail, and it refuses (and revokes) any token that can.

Gmail cannot call your server directly. It publishes to a Pub/Sub topic, and a push subscription on that topic calls your server. You create both.

In Cloud Shell, with your values:

Terminal window
PROJECT=my-project
HOST=https://teamhud.example.com
gcloud config set project $PROJECT
# 1. The topic Gmail publishes to
gcloud pubsub topics create teamhud-gmail
# 2. Let Gmail publish to it
gcloud pubsub topics add-iam-policy-binding teamhud-gmail \
--member=serviceAccount:gmail-api-push@system.gserviceaccount.com \
--role=roles/pubsub.publisher
# 3. An identity for Pub/Sub to sign its calls to your server with
gcloud iam service-accounts create teamhud-gmail-push \
--display-name="TeamHUD Gmail push"
# 4. The push subscription. The audience MUST be exactly this URL.
gcloud pubsub subscriptions create teamhud-gmail-push \
--topic=teamhud-gmail \
--push-endpoint=$HOST/webhooks/gmail/push \
--push-auth-service-account=teamhud-gmail-push@$PROJECT.iam.gserviceaccount.com \
--push-auth-token-audience=$HOST/webhooks/gmail/push \
--ack-deadline=10

Then in .env:

Terminal window
GMAIL_PUSH_TOPIC=projects/my-project/topics/teamhud-gmail
GMAIL_PUSH_SA_EMAIL=teamhud-gmail-push@my-project.iam.gserviceaccount.com
Terminal window
docker compose up -d
docker compose logs server | grep gmail-watch
# [gmail-watch] enabled (6h renewal watch)

Existing Gmail users must disconnect and reconnect Gmail once. Push is armed at connect time, and they connected before the topic existed. After that the server re-arms every watch before Google’s 7-day expiry by itself, and drops registrations nobody has used for 30 days.

How your server trusts a push. Every call carries a Google-signed token. The server accepts it only if it was issued for the service account in GMAIL_PUSH_SA_EMAIL and its audience is exactly PUBLIC_BASE_URL + /webhooks/gmail/push. There is no setting to change that audience. If PUBLIC_BASE_URL changes, update the subscription:

Terminal window
gcloud pubsub subscriptions update teamhud-gmail-push \
--push-endpoint=$NEW/webhooks/gmail/push \
--push-auth-token-audience=$NEW/webhooks/gmail/push
  • “One or more users named in the policy do not belong to a permitted customer” at step 2: your org enforces Domain restricted sharing (constraints/iam.allowedPolicyMemberDomains), and Gmail’s publisher account is outside your domain. An org-policy admin must add an exception for this project.
  • Step 4 permission denied: you need Service Account User on teamhud-gmail-push. Projects created before April 2021 also need the Pub/Sub service agent (service-<project-number>@gcp-sa-pubsub.iam.gserviceaccount.com) granted Service Account Token Creator.
  • One project per TeamHUD server. Two servers watching the same mailbox through the same project steal the watch from each other, so push rings on whichever armed it last. Give staging its own project.

If your Workspace restricts third-party apps (Admin console → Security → Access and data control → API controls), mark this OAuth client Trusted. An Internal app is usually allowed already, but “unconfigured third-party apps: restricted” can still block Gmail’s restricted scopes.

This is also where you see and revoke every grant your users have made.

Symptom Cause
Consent screen shows TeamHUD’s app, not yours The desktop is not pointed at your server. See INTEGRATIONS.md
redirect_uri_mismatch The client’s redirect URI differs from GOOGLE_REDIRECT_URI (or from PUBLIC_BASE_URL + /oauth/google/callback if unset). Also check the client type is Web application
Connect fails with google_oauth_not_configured GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET did not reach the container. Check .env, then docker compose up -d
Error 403: org_internal The user is not in your Workspace org. Internal apps admit org members only
access_denied / “has not completed the Google verification process” External app in Testing, and the user is not a test user
invalid_scope or “API has not been used in project” That API is not enabled (step 2)
Users reconnect every week External app in Testing. Move to Workspace + Internal
Mail arrives only on poll, [gmail-watch] disabled (GMAIL_PUSH_TOPIC unset) Step 6 not done
Server log shows token_exchange_failed on Gmail connect The desktop got its token from TeamHUD’s hosted app, not yours. Point it at your server, then reconnect
Pub/Sub subscription shows failed deliveries with 401 Audience or service account mismatch. Compare the subscription’s audience with PUBLIC_BASE_URL + /webhooks/gmail/push, and its service account with GMAIL_PUSH_SA_EMAIL
Push used to work and stopped The token backing it was revoked or expired (External/Testing: 7 days). Reconnect Gmail
  • Sign in with Google. SOCIAL_PROVIDERS is empty by default and can list google, but this bundle does not configure Google in GoTrue, so no button appears. Use Slack or email + password.