A Gmail-connected app built with Cursor, Claude Code, Lovable, Replit, or Bolt is ready to launch when six things hold: the scope list is the narrowest that works, refresh tokens are encrypted server-side and bound to one user, no code path can use one user’s token for another, the OAuth consent screen is published and verified (with CASA scheduled if a scope is restricted), the watch() subscription is renewed at least every 7 days, and disconnecting a user leaves nothing behind. Until the consent screen is verified, Google caps you at 100 test users and every token dies 7 days after consent. None of it needs a rewrite.

TL;DR

Gmail-connected apps built with AI coding tools fail in six predictable places. Check these before launch, in this order:

  1. Scopes: ask for the narrowest Gmail scope, not full access
  2. Token storage: refresh tokens encrypted, server-side, per user, revocable
  3. Per-user isolation: one user can never read another user’s messages, even through a bug
  4. Google verification and CASA: without it you are capped at 100 test users and 7-day tokens
  5. Watch renewals and quotas: push notifications expire after 7 days; new projects have per-user quota ceilings since May 2026
  6. Deletion and data policy: delete means delete, including Google’s Limited Use rules

The path from scope to launch

The scope the AI picked decides everything downstream: whether you need verification at all, whether you need a CASA assessment, and how long the calendar is. This is the path, from Google’s own scope classification (updated July 22, 2026).

flowchart LR S["Which Gmail scope
does the app request?"] --> N["Non-sensitive
gmail.labels, add-on scopes"] S --> SE["Sensitive
gmail.send"] S --> R["Restricted
gmail.readonly, gmail.modify,
gmail.metadata, mail.google.com"] N --> P1["Publish"] SE --> V1["Brand + scope
verification"] --> P2["Publish"] R --> V2["Brand + scope
verification"] --> C["CASA, AL1 or AL2,
revalidated every 12 months"] --> P3["Publish"] S -. "consent screen
still in Testing" .-> T["100 test users,
every token dies after 7 days"]

The six checks below follow that path. Each one names the symptom you will see in an AI-built app and the one thing to verify.

1

Scopes

Full access requested because it made every feature work. Check: the scope list in code and in the OAuth client match, and neither contains mail.google.com unless you delete mail.
2

Token storage

Refresh tokens in a plain-text column, in logs, or in the browser. Check: every read of refresh_token, traced to its destination.
3

Per-user isolation

The Gmail client is built from a request parameter or a shared variable. Check: a two-user test proves no call for user A carries user B’s token.
4

Verification and CASA

Consent screen still in Testing at launch. Check: publishing status in Cloud Console, and a CASA lab booked if any scope is restricted.
5

Watch renewals and quotas

watch() called once at setup. Check: a daily renewal job, historyId reconciliation, and a backfill that respects 6,000 units per user per minute.
6

Deletion and Limited Use

Message bodies cached and kept after disconnect. Check: disconnect a test account, then query the database.

1. Scopes: the AI asked for more than you need

Gmail’s API has a ladder of OAuth scopes, and Google classifies each rung. The classification, not the feature, decides your verification path.

ScopeWhat it allowsGoogle’s classWhat launch requires
gmail.labelsRead and edit labelsNon-sensitivePublish
gmail.sendSend mail as the userSensitiveBrand and scope verification
gmail.readonlyRead messages and settingsRestrictedVerification plus CASA
gmail.modifyRead, compose, send, label, archive (no permanent delete)RestrictedVerification plus CASA
gmail.metadataHeaders and labels, no bodiesRestrictedVerification plus CASA
mail.google.comEverything, including permanent deletionRestrictedVerification plus CASA, and the hardest review

AI coding tools tend to request mail.google.com because every feature works on the first try. Three problems follow. Users see a scarier consent screen and drop off. Google’s reviewers ask you to justify every scope, and “the AI picked it” is not a justification. And a breach of a full-access token is a breach of the whole mailbox.

The check: open your OAuth client configuration and your code’s scope list. If mail.google.com is there and you do not delete messages, remove it. If the app only sends, gmail.send alone keeps you out of CASA entirely. Expect to re-verify after any scope change.

2. Token storage: where the refresh tokens actually live

A Gmail refresh token is a standing key to someone’s inbox. The pattern in AI-built apps, seen repeatedly: the token is written to a database column in plain text, sometimes logged on every request, occasionally shipped to the browser so the frontend can “call Gmail directly.”

What has to hold: tokens stored server-side only, encrypted at rest with a key from a KMS or vault, not a constant in the code. Each token bound to exactly one user record, looked up by the authenticated session, never by a client-supplied user id. Logs redact tokens and message bodies. Deleting the account revokes the token at Google (oauth2.revoke) and destroys the stored copy.

The check: search the repo and the database schema for refresh_token. Trace every read of that column. If any path returns it to the client or writes it to a log, that is finding number one of your teardown.

3. Per-user isolation: the bug that leaks someone else’s inbox

Every Gmail-connected app has a place where it decides whose token to use. In AI-built code that decision is often made from a parameter in the request (?userId=), from a cached “current user” variable that survives across requests, or from a background job that iterates users and reuses one client object.

This is the failure mode that turns a bug into a breach: user A’s sync job runs with user B’s token, or a shared client retains the last user’s credentials. It is invisible in single-user testing, and it is the first thing a technical due diligence reviewer looks for.

The check: find every construction of the Gmail client. Confirm it is built from the token of the authenticated user in that request or that job, and destroyed after. Then write the one test that matters: two users, two inboxes, assert that no call for user A ever carries user B’s token.

4. Google verification and the CASA assessment

Until your OAuth consent screen is published and verified, the project stays in Testing: a maximum of 100 test users, and every authorization, refresh token included, expires 7 days after consent. Founders hit this the week after launch, when the first cohort’s connections silently die.

Any restricted Gmail scope requires more than brand verification: a CASA (Cloud Application Security Assessment) by a Google-authorized lab, and Google states that apps must be revalidated at least every 12 months after the assessor’s letter date. CASA now assigns one of two assurance levels: AL1, where the lab validates evidence and scan results you submit, and AL2, where the lab tests the running application, its infrastructure, and every place user data is stored. Google, not you, picks the level, based on scopes, user volume, and risk signals. Google’s own guidance is that restricted-scope verification “potentially takes several weeks.”

The one thing to check today
Open Google Cloud Console, then APIs and Services, OAuth consent screen (Audience). If the publishing status says Testing, your launch has a 100-user ceiling and a 7-day token lifetime right now, whatever the code does. Start the verification this week; the calendar, not the engineering, is the long pole.

5. Watch renewals and the 2026 quota tiers

If the app reacts to new mail, it uses users.watch() to get push notifications through Pub/Sub. Google’s documentation is direct: call watch at least once every 7 days or updates stop, and the recommendation is once per day. Expiry is silent; there is no error and no email. After any gap, catch up with history.list from the last stored historyId.

How the AI wired it
watch() is called once, in the onboarding handler. Nothing renews it. The demo works for a week, then every user goes quiet on day 8 and nobody knows why. The last historyId was never stored, so there is nothing to reconcile from.
What a product does
A daily job calls watch() for every connected user and stores the returned expiration. Each notification’s historyId is persisted. On restart or after any gap, history.list(startHistoryId) replays what was missed. An alert fires if a user’s watch is within 24 hours of expiry.

There is a second, newer ceiling. On May 1, 2026 Google rolled out a tiering model for Gmail API usage. Cloud projects created on or after that date get 1,200,000 quota units per minute per project and 6,000 per minute per user, with an 80,000,000-unit daily threshold. messages.get costs 20 units, threads.get 40, users.watch 100, messages.send 100. Projects that used the API between November 2025 and April 2026 keep their earlier quotas.

That per-user number matters for a common AI-built pattern: on connect, fetch every message in the inbox. At 20 units each, 300 messages.get calls fill a user’s minute. A 10,000-message inbox backfilled that way takes 33 minutes of quota errors unless the job batches, paces, and resumes.

The check: search for watch(. If there is no scheduled renewal and no reconciliation on historyId, the integration is a demo, not a product. Then look at the first sync: does it page, back off on 429s, and prefer messages.list plus history.list over fetching every body?

6. Deletion, retention, and Google’s Limited Use rules

Gmail data falls under Google’s API Services User Data Policy and the Workspace API user data policy (updated July 22, 2026). The Limited Use requirements: use the data only to provide or improve user-facing features prominent in your app, do not sell or transfer it, do not use it for ads or credit decisions, do not let humans read it except with the user’s specific agreement, and do not use it to create, train, or improve an AI or ML model beyond that one user’s personalized feature. Your privacy policy has to say this, and the app has to do it.

AI-built apps commonly cache message bodies “for performance,” keep them after the user disconnects, and feed them to a third-party model without a word in the privacy policy.

The check: disconnect a test account and look at the database. Anything left is a policy problem and, for employment or health data, a legal one. Then read the privacy policy against the code: every data flow the code performs must be disclosed.

What “ready” looks like

A Gmail-connected app is launch-ready when: the scope list is minimal and justified; tokens are encrypted, server-side, per user, and revoked on deletion; the two-user isolation test passes; the consent screen is verified, with the CASA lab booked if a scope is restricted; watches renew daily and the first sync respects the per-user quota; and disconnect leaves nothing behind.

None of this requires a rewrite. In most AI-built apps it is a fixed-scope hardening pass of one to two weeks, done once, with the trail left behind: the isolation test, the renewal job, the runbook for the day Google emails you about your revalidation.

If you want senior eyes on your actual code first, that is what the free teardown is: three findings and a verdict, recorded, within 72 hours.

Sources