Killing the Global Token: Per-Org OAuth for Ads, GSC, and GA4
Summary
Our multi-client platform read Ads/GSC/GA4 data through one global OAuth token in .env, risking cross-client data leakage and restart-based config changes. We replaced it with per-org OAuth: per-org tokens in the DB, real-time resolution per request, org-scoped caches, and self-service re-auth.
Quick answer
Stop sharing one global token. Store refresh tokens per org in the database, resolve the current org's token from the DB on every request (no restart), scope every cache key by org, and let orgs re-authorize themselves through a settings page when a token expires.
In our multi-client marketing platform, every customer's Google data — Ads, Search Console, and GA4 — was once read through a single global OAuth token stored in a .env file. It worked, until we thought about what could go wrong: client A's ad data pulled into client B's dashboard. This is the story of how we killed that global token for good.
The risks we were living with
One shared credential created two problems. First, data isolation: with every org reading through the same token, a misrouted request or a wrong mapping could expose one client's numbers to another — the worst possible failure in a data product. Second, operations: changing the credential meant editing config and restarting the service, which meant planned downtime for a change that should be invisible.
The fix: per-org OAuth
We moved to a model where each organization authorizes Google on its own account. The three channels — Ads, GSC, and GA4 — each carry independent connection status per org. Refresh tokens are stored per org in the database, and every request resolves the current org's token from the database in real time. No shared credential, no restart to pick up a change, and the .env fallback was removed entirely. The switch was not a big-bang cutover: org authorization state, token lifecycle, and cache behavior were each validated on one channel first, then replicated to the rest.
How the details stay safe
- Expired tokens self-serve. When a token fails, the org reconnects through its settings page. One org's re-auth never touches another org's data.
- Caches are org-scoped. Every cache key includes the org dimension, so no cached payload can ever leak across tenants.
- Scheduled jobs resolve per org. Background syncs read the org's current token through a singleton that hits the database, not a global config.
We rolled the refactor out in two phases — GSC and GA4 first, Ads last — so each channel could be verified independently before the next one switched. The old credential in .env was only deleted after every org had reconnected through the new flow.
Reusable checklist: multi-tenant OAuth in 4 rules
- Store credentials per org. Tokens live in the database, keyed by org — never in a shared file.
- Resolve at request time. Read the current org's token from the database on every request, so changes apply instantly.
- Scope every cache. Cache keys must include the org dimension to prevent cross-tenant leakage.
- Make re-auth self-service. A failed token should route the user to a reconnect flow, not to you.
If your platform is still reading multi-tenant data through one shared credential, our marketing analytics platform went through exactly this refactor — per-org OAuth included.
Frequently asked questions
What was wrong with a single global token?
Two things: data isolation — every org reads through the same credential, so a misrouted request can leak one client's data to another; and operations — changing the credential meant editing config and restarting the service.
Why resolve tokens from the database per request?
Because credentials change. Real-time resolution means a re-auth or a new org takes effect immediately, with no restart and no shared state to invalidate.
How do caches stay cross-tenant safe?
Every cache key includes the org dimension, so a payload cached for one org can never be served to another — even on a cache hit.