Skip to content
Esc
↑↓navigate↵open⌘Jpreview
On this page

Gmail app example

Copy an app that links personal Google accounts with Better Auth and D1.

Deploy the Gmail example to let users connect their own Google accounts and read Gmail from your apps. It includes a connection page and persistent account storage; apps access Gmail through an app service binding. The runtime carries the verified company identity from the calling app; Gmail never accepts a user ID supplied as an RPC argument.

Prerequisites

  • An organization administrator account and a signed-in Widefleet CLI.
  • Matching control-plane, CLI, deployment-agent and app-runtime releases at 0.5.0 or newer for app services and verified callers. Configure organization sign-in and a public HTTPS app address first.
  • Node.js 24 or newer and pnpm 12.4.2 for the app project.
  • A Google Cloud project where you can enable Gmail API and create an OAuth client.

Copy and deploy

Copy the complete example directory, including its lockfile and package-manager configuration, into your own repository:

git clone --depth 1 --branch v0.5.0 https://github.com/widefleet/widefleet.git
cp -R widefleet/examples/apps/gmail gmail-app
cd gmail-app
pnpm install --frozen-lockfile
pnpm typecheck
pnpm test

In Google Cloud, enable Gmail API, configure the OAuth consent screen and create a Web application OAuth client. For an external application in Testing mode, add your intended Google accounts as test users. Set GOOGLE_CLIENT_ID in wrangler.jsonc to that client’s ID, then deploy:

widefleet deploy --skip-build
widefleet apps

Wait for the app’s activation to succeed. Open Gmail from Apps and copy its HTTPS URL. The app’s callback path is /auth/callback/google. For example, an app URL of https://gmail-team.apps.example.com/ requires this exact Authorized redirect URI in your Google OAuth client:

https://gmail-team.apps.example.com/auth/callback/google

The same callback serves all apps connected to Gmail.

Allow the Google endpoints from the Gmail app’s backend network policy. Sign in with network:manage in addition to the deployment scopes:

widefleet login --scope platform:read platform:write network:manage
widefleet network allow https://oauth2.googleapis.com https://www.googleapis.com https://gmail.googleapis.com --app gmail

The OAuth page navigates to Google; it needs no browser network grant. Set these secrets in Apps → gmail → Secrets:

Secret Value
GOOGLE_CLIENT_SECRET The secret of the Google OAuth client above.
AUTH_SECRET A persistent random secret of at least 32 characters, generated with a password manager or cryptographic random generator.

Alternatively, put each value in a protected file outside the project and use:

widefleet secret put GOOGLE_CLIENT_SECRET --app gmail --file /absolute/path/to/google-client-secret.txt
widefleet secret put AUTH_SECRET --app gmail --file /absolute/path/to/auth-secret.txt

Back up AUTH_SECRET with the app’s D1 data: it encrypts saved tokens, and replacing it requires users to reconnect. Deployment provisions D1; the app initializes its tables on first use.

The requested scopes are Google profile, email and gmail.readonly. Google classifies gmail.readonly as a restricted scope. Check the Google verification requirements for your application’s audience and data use before distributing it beyond testing.

Connect a user and an app

Open the Gmail app URL, sign in through the organization’s identity provider, then choose Connect Google account. Select a Google account and approve the requested permissions. The callback returns to the app page, which displays the linked email address. Return to your app and retry its Gmail action.

Give an existing app the binding:

widefleet connect gmail --as GMAIL --app inbox

The app’s server code can call:

const result = await env.GMAIL.listMessages({ maxResults: 20 });

Use these app methods:

Method Result
connectionStatus() { connected, googleAccountId } describing the saved link; it does not check token validity with Google.
listMessages({ maxResults?, pageToken? }) On success, { status: "connected", messages, nextPageToken? }. Each message has id and threadId. The default page size is 20; the example permits 1–100.
getMessage(id) On success, { status: "connected", message } with the message ID, thread ID, snippet and headers. It requests metadata, not the full message body.

The Gmail methods return a status when user action or a retry is needed:

Status App behavior
connection_required Show the connection link.
permission_required Ask the user to reconnect and grant Gmail access.
refresh_failed Offer a retry and a reconnection link; the failure may be temporary.
reconnect_required Google rejected the access token with HTTP 401; offer reconnection.
access_denied Google returned HTTP 403. Check granted scopes, Gmail API enablement and Google Workspace policy.

Other errors, including rate limits and provider outages during a Gmail API call, reject the RPC call. Apply the app’s usual error handling. Never render message snippets or header values as unescaped HTML.

The app can open the app page in another tab so its own state remains available:

<a href="https://gmail-team.apps.example.com/" target="_blank" rel="noopener noreferrer">
  Manage Gmail connection
</a>

Use the deployed Gmail app URL to open the connection page; its buttons submit the connection and disconnection requests. After connecting, the user returns to the app and retries the Gmail action.

Manage connections

Each signed-in user links one Google account, shared across apps using this app. To switch accounts, choose Disconnect first. A Google account can belong to one Widefleet user in this app at a time.

Disconnect removes the saved tokens and linking sessions. To revoke the provider grant as well, remove the connection in your Google account settings. Reconnecting the same account updates its credentials.

D1 data survives deployments. Keep the app name and D1 binding stable, and back up the data together with AUTH_SECRET. If you customize the database schema, include a migration for existing data.

For testing and public distribution, review Google’s scope verification requirements and token expiration rules.

Adapt to another provider

Keep the caller identity and account-linking checks. Replace the provider configuration in src/google.ts, the credentials in wrangler.jsonc and src/context.ts, and the API methods in src/app.ts. Update the page labels, cookie prefix and tests for your provider.

Register /auth/callback/<providerId> at the new app origin, add its credentials, and run pnpm typecheck, pnpm test and pnpm build. Check your provider’s scopes, profile fields and refresh-token behavior; see Better Auth Generic OAuth.

Deploy the adapted app, connect a test account at its browser address, and call a method from an app bound to it.

Last updated on October 29, 2026