Fixing the OAuth 7-Day Token Expiry: Moving from Testing to Production in Google Cloud

We've been caught in a recurring authentication nightmare: every seven days, our Google Calendar and Gmail integrations die. The symptom is always the same—refresh tokens expire, triggering a manual re-auth cycle through the Google consent screen. We did this "fix" before, but the root cause was never addressed. This post documents the permanent solution: moving our Google Cloud OAuth application from Testing mode to Production mode, which removes the 7-day refresh token expiry hard cap.

The Problem: Testing Mode's Built-In Expiry

Google Cloud's OAuth consent screen has two states: Testing and Production. When your app is in Testing mode, Google hard-caps refresh token lifetime to 7 days—this is by design, to prevent test apps from lingering in users' OAuth grants indefinitely. We had a test app, added production service accounts and Lambda functions that needed persistent calendar access, but never promoted the app to Production status. The mismatch caused Lambda to fail silently when refresh tokens expired, breaking calendar syncs across Boatsetter, Sailo, and internal JADA booking flows.

Technical Details: OAuth Consent Screen Configuration

Current state: The Google Cloud project (tied to our Apps Script deployment for BookingAutomation) had the consent screen configured but stuck in Testing mode. This is a UI-only setting in the Google Cloud Console—there's no API to change it programmatically, which is why our infrastructure-as-code approaches hit a wall.

What needs to happen:

  • Navigate to Google Cloud Console → OAuth consent screen
  • Change app status from "Testing" to "In production"
  • Verify that all required scopes are declared (Calendar, Gmail, Sheets for JADA's use case)
  • Submit for Google verification (typically takes 24–48 hours for apps with straightforward OAuth flows)
  • Once approved, refresh tokens issued to service accounts and Lambda functions will no longer expire after 7 days

The verification step is non-negotiable. Google requires that you demonstrate your app's purpose, the scopes you use, and how you handle user data. For us, this is straightforward: we're a sailing charter service managing bookings and crew coordination. Our scopes are:

  • https://www.googleapis.com/auth/calendar — read/write booking events to JADA Internal calendar
  • https://www.googleapis.com/auth/gmail.readonly — check for booking confirmations and supplier responses
  • https://www.googleapis.com/auth/spreadsheets — sync crew assignments and manifest data

Why This Wasn't Done Before: The Context

The original setup likely used Testing mode because it's faster for development. Testing mode doesn't require Google verification and lets you iterate quickly. However, once you deploy Lambda functions and serverless triggers that need persistent auth, Testing mode becomes a liability. The previous "fix" was a band-aid: re-auth the token when it expired, which buys you another 7 days. This creates the recurring cycle we've been in.

The decision to move to Production now is driven by operational reality: we have two calendar sync systems (CalendarSync.gs in Apps Script + Lambda calendar endpoint in API Gateway) that rely on persistent refresh tokens. Every 7 days of downtime is unacceptable.

Infrastructure Impact: Token Refresh in Lambda

Once the app moves to Production and refresh tokens stop expiring, our Lambda functions can continue using the same token indefinitely. The current token storage and refresh logic is:


Token storage: /opt/lambda/calendar_token.json on Lightsail (synced from DynamoDB)
Lambda handler: /opt/lambda/calendar_sync.py
Refresh logic: Google OAuth2 library handles auto-refresh on token.refresh()

The relevant Lambda environment variables (stored in CloudFormation or API Gateway):

  • GOOGLE_OAUTH_CLIENT_ID — OAuth 2.0 application ID
  • GOOGLE_OAUTH_CLIENT_SECRET — OAuth 2.0 client secret
  • GOOGLE_CALENDAR_ID — JADA Internal calendar ID (jadasailing@gmail.com)
  • TOKEN_FILE_PATH — /opt/lambda/calendar_token.json

No changes needed to the Lambda code itself. The token refresh library will simply... keep working, instead of failing after 7 days.

CalendarSync.gs: Activation Blocker

We have a separate issue compounding the problem: the Boatsetter iCal sync trigger in Apps Script exists but was never activated. The code in /Users/cb/Documents/repos/tools/CalendarSync.gs includes a setup function:


function calendarDashboardSetup() {
  // Install onEdit triggers for JADA Internal calendar
  // Install time-based triggers for Boatsetter iCal polling
  // Initialize state.json with sync metadata
}

This function must be executed once from the Apps Script editor to install the triggers. Until it runs, Boatsetter bookings don't auto-sync—they require manual calendar updates. The ticket m-91325edb ("Set up Boatsetter iCal sync") has been sitting in needs-you because the execution step was documented but not tracked operationally.

Once the OAuth app moves to Production and tokens stop expiring, the CalendarSync triggers will continue to fire without interruption.

Multi-Platform Calendar Blocking: May 30 Case Study

The May 30 Boatsetter booking exposed gaps in our multi-platform sync strategy:

  • Boatsetter: Uses iCal feed. Sync depends on CalendarSync.gs triggers being active + valid OAuth tokens. Currently manual update required.
  • Viator: API integration stalled awaiting their response to integration emails (ticket t-ad4b92d7). Until resolved, manual blocking in supplier.viator.com.
  • Sailo: Also consumes JADA iCal feed. Works once CalendarSync.gs is activated.
  • GetMyBoat: Listing not yet live, no sync needed.

The root issue: we have the plumbing (iCal feed, API endpoints, trigger logic) but activations and OAuth tokens are blocking. Fixing OAuth moves us from a "requires manual updates every 7 days" model to a "set it once and forget it" model.

Action Items & Timeline

  • Immediate (today): Navigate to Google Cloud Console, change OAuth consent screen to "In production," submit for verification.
  • 24–48 hours: Await Google's verification email. No code changes needed during this window.
  • Post-approval: Issue a fresh set of refresh tokens (via OAuth flow or service account key) and push to Lambda environment variables. Old tokens continue to work, but new ones won't expire.
  • Parallel: Execute calendarDashboardSetup() in Apps Script editor to activate Boatsetter sync triggers.
  • Validation: Test calendar sync without manual intervention for 14