We built an AI-powered CRM that sends emails, manages deals, tracks deliverability, and alerts our team via Google Chat. Every one of those features touches a different Google API. And every one of those APIs lives behind a different permission gate, managed in a different admin console, governed by a different set of rules.
This is the story of how a system that worked perfectly on day one slowly fractured into three separate auth profiles with mismatched scopes, silent failures, and a re-auth process that broke more than it fixed.
It Started Simple
AgentCRM began as a Google Chat bot. One OAuth client, one scope (chat.messages), one profile. Claude responds to CRM queries in a Chat space. Clean.
Then we added email generation — personalized outreach emails with HTML signatures, tracking pixels, Gmail drafts. That needed gmail.modify. Different API, different scope, different consent. We created a second profile (CRM1) for the sales sender.
Then Drive uploads for account handoff documents. That needed drive. We added it to the existing profile.
Then email delivery tracking — polling Google's Admin Reports API to find out if our emails actually landed. That needed admin.reports.audit.readonly. A restricted scope. A different API entirely. One that requires workspace admin privileges and has its own enablement flow.
Then we moved the Chat bot into a Docker container and needed a third profile (docker-work) with file-based credential storage because containers don't have system keyrings.
Each capability was individually reasonable. Each addition was a clean PR. But the aggregate was a mess.
The Permission Archaeology
Here's what three months of incremental feature development produced:
| Profile | User | Scopes | What Broke |
|---|---|---|---|
work |
michael.paige@ | cloud-platform, drive | Missing gmail.modify (drafts fail), missing admin.reports (delivery tracking 403s) |
CRM1 |
ryan.slipakoff@ | gmail.modify, drive | Only profile that actually worked correctly |
docker-work |
michael.paige@ | 7 chat sub-scopes, cloud-platform | Missing gmail.modify, missing drive |
Two of three profiles were degraded. Features that appeared to work were actually routing through the wrong profile or silently skipping steps.
Google's Five Layers of Permission
What makes this particularly insidious is that Google doesn't have one permission system. They have at least five, and you need all of them aligned for a single API call to succeed:
Layer 1: GCP API Enablement
Where: Google Cloud Console → APIs & Services → Library
Before any OAuth scope does anything, the underlying API must be enabled on the GCP project. Gmail API, Drive API, Chat API, Admin SDK — each is a separate toggle. If Admin SDK isn't enabled, requesting the admin.reports.audit.readonly scope during OAuth just silently... doesn't grant it.
No error. No warning. The consent screen shows fewer scopes than you asked for, and unless you're counting, you won't notice.
Layer 2: OAuth Consent Screen Scope Registration
Where: Google Cloud Console → APIs & Services → OAuth consent screen → Edit → Scopes
Even after the API is enabled, the scope must be registered on the consent screen. This is a different step from enabling the API. You can have the Gmail API enabled but gmail.modify not listed on the consent screen. The OAuth flow will just drop that scope from the token.
Restricted scopes (like admin.reports.audit.readonly) have additional review requirements. For internal Workspace apps this is less onerous, but it's still a separate configuration surface.
Layer 3: OAuth Token Scopes
Where: The OAuth flow itself (browser consent screen)
When you run gws auth login -s gmail.modify,drive,chat, the CLI requests those scopes. But here's the trap: GWS CLI replaces scopes on re-auth, it doesn't append. If you re-auth to add one scope, you lose all the others unless you specify every scope in a single command.
We learned this the hard way. Re-authing to add admin.reports.audit.readonly dropped gmail.modify and chat.messages. The delivery tracking we were trying to fix broke the email drafting that was working.
Layer 4: Workspace Admin — Trusted App Status
Where: Google Workspace Admin Console → Security → API controls → App access control
Google's Re-Authentication Policy (RAPT) enforces periodic re-auth for OAuth apps that aren't marked as "Trusted" by the workspace admin. This manifests as invalid_rapt errors after 24 hours — your refresh token is fine, your scopes are fine, but Google forces an interactive re-auth anyway.
This is managed in the Workspace Admin Console, not the GCP Console. Different URL, different UI, different permission model. You need workspace admin access, not just GCP project owner access.
The fix is adding the OAuth client ID as a "Trusted" app. But finding this setting requires navigating: admin.google.com → Security → API controls → App access control → Manage Third-Party App Access → Configure new app → OAuth App Name or Client ID. That's six clicks deep in an admin console that most developers never open.
Layer 5: Per-User Scope Consent
Where: The user's own Google account security settings
Each user who authenticates through the OAuth flow has their own consent record. If a user previously granted 3 scopes and you add 2 more to the consent screen, the user needs to re-authenticate to grant the new scopes. Existing tokens won't magically gain new permissions.
And if the user revokes access from their Google Account settings (myaccount.google.com → Security → Third-party apps), all scopes are gone and re-auth is required — but the GCP and Workspace admin settings remain unchanged.
The Compound Failure Mode
The interaction between these layers creates failure modes that are genuinely difficult to debug:
Scenario: You add Admin SDK API support for delivery tracking.
- You enable Admin SDK API in GCP ✓
- You add
admin.reports.audit.readonlyto the consent screen ✓ - You run
gws auth login -s admin.reports.audit.readonly✓ - The API returns 403
insufficientPermissions✗
What happened: Step 3 replaced all existing scopes with just the one you specified. Your Gmail drafts stopped working too, but you don't notice because you're focused on the Admin SDK error.
Another scenario: Everything works for 24 hours, then Chat messages stop sending.
- OAuth scopes are correct ✓
- API is enabled ✓
- Token is valid ✓
invalid_rapterror ✗
What happened: The OAuth app isn't Trusted in Workspace Admin. RAPT kicked in after 24 hours. Nothing in the GCP Console or OAuth flow would have told you this. The error message doesn't mention RAPT, Workspace Admin, or Trusted apps — it just says "invalid_rapt" and you're left to Google that string.
How We Fixed It
We stopped treating auth as something you configure once and forget. We built a system around it:
1. Canonical scope manifest — A single TypeScript file (profiles.ts) defines which profile needs which scopes. This is the source of truth, not whatever scopes happen to be on the token.
2. Deterministic re-auth script — gws-reauth.sh reads the canonical scope set and passes all scopes in one command. No more "add one scope, lose three" accidents.
3. Health check — An MCP tool and HTTP endpoint that compares actual token scopes against the canonical set. Reports missing scopes with exact fix commands.
4. Documentation — A single doc that maps every Google integration to its profile, scopes, APIs, admin settings, and failure modes. Step-by-step GCP Console instructions because the UI changes frequently enough that "go to the scopes page" isn't sufficient.
The health check output looks like this:
work ([email protected]) — WARN
Token: valid
Scopes: MISSING: gmail.modify, chat.messages, admin.reports.audit.readonly
Fix: ./scripts/gws-reauth.sh work
CRM1 ([email protected]) — OK
Token: valid
Scopes: all present
docker-work ([email protected]) — WARN
Token: valid
Scopes: MISSING: gmail.modify, drive
Fix: ./scripts/gws-reauth.sh docker-work
The Lesson
Google's APIs are individually well-designed. The OAuth flows work. The admin consoles are functional. But the aggregate experience of building a system that touches Gmail + Drive + Chat + Admin SDK across multiple users is genuinely complex in ways that aren't documented in any single place.
Five separate admin surfaces. Five layers that all need to agree. Silent scope dropping. Scope replacement on re-auth. An admin setting six clicks deep that causes 24-hour auth expiry. Error messages that require you to know the acronym "RAPT" to debug.
The answer isn't to avoid Google APIs — they're powerful and our CRM depends on them. The answer is to treat auth as infrastructure, not configuration. Monitor it. Codify the expected state. Detect drift. Automate recovery.
Because if you're building anything that touches more than two Google APIs, your auth will fragment. The only question is whether you'll notice before your users do.
Built with AgentCRM by United Logic. We build AI-powered systems on Adobe and Google platforms — and we've learned where the edges are.