I spent an afternoon debugging why Google OAuth was completely non-functional on a deployed Next.js app. No errors in the logs. No build failures. The app loaded fine, magic link login worked, but the Google sign-in button simply did nothing. The provider was not initializing.
The root cause was a three-character difference in a YAML file.
This guide documents the configuration layer between Firebase App Hosting, GCP Secret Manager, and NextAuth.js — specifically the subtle mismatches that cause silent failures. If you are deploying NextAuth.js apps on Firebase App Hosting, this will save you hours.
The Architecture
Firebase App Hosting deploys Next.js apps to Cloud Run with an apphosting.yaml configuration file that maps GCP Secret Manager secrets to environment variables. NextAuth.js reads those environment variables at runtime to configure its authentication providers.
The chain looks like this:
GCP Secret Manager --> apphosting.yaml --> process.env.* --> NextAuth config
(stores the value) (maps secret to var) (runtime env var) (reads the var)
Every link in this chain must match exactly. If any link uses a different name, the value never reaches NextAuth, and Google OAuth silently disables itself.
The Silent Failure
NextAuth.js v5’s provider system is designed to be additive. You push providers onto an array, and if a provider’s credentials are undefined, you simply don’t add it:
// packages/auth/config.ts (simplified)
if (googleClientId && googleClientSecret) {
providers.push(
Google({
clientId: googleClientId,
clientSecret: googleClientSecret,
}),
);
}
This means if the environment variables are missing or named wrong, the Google provider never gets added. No error. No warning. The app works perfectly fine for email-based login. You would only notice the problem if you specifically tested Google sign-in on the deployed app.
Failure Mode 1: Variable Name Mismatch
This is the most common and the most dangerous failure because it is invisible.
The wrong way
# apphosting.yaml -- WRONG
env:
- variable: AUTH_GOOGLE_ID # <-- wrong name
secret: myapp-google-id
availability:
- RUNTIME
- variable: AUTH_GOOGLE_SECRET # <-- wrong name
secret: myapp-google-secret
availability:
- RUNTIME
// lib/auth.ts -- reads different names
googleClientId: process.env.GOOGLE_CLIENT_ID, // undefined!
googleClientSecret: process.env.GOOGLE_CLIENT_SECRET, // undefined!
The YAML injects the secret value into AUTH_GOOGLE_ID, but the code reads GOOGLE_CLIENT_ID. The value exists in the container’s environment under the wrong key.
The right way
# apphosting.yaml -- CORRECT
env:
- variable: GOOGLE_CLIENT_ID
secret: myapp-google-client-id
availability:
- RUNTIME
- variable: GOOGLE_CLIENT_SECRET
secret: myapp-google-client-secret
availability:
- RUNTIME
The variable name in apphosting.yaml must match exactly what the application code reads from process.env.
This sounds obvious written down. In practice, it happens because:
- Different people set up the YAML and the auth code
- NextAuth’s own documentation uses
AUTH_GOOGLE_IDin some examples - The naming convention you chose when creating secrets (
myapp-google-id) bleeds into the variable name - Copy-paste from a different app that uses different naming
Failure Mode 2: Wrong Availability Flags
Firebase App Hosting has two availability contexts for environment variables:
- BUILD: Injected during Cloud Build (when
next buildruns) - RUNTIME: Injected into the Cloud Run container (when requests are served)
Which variables need which flags
| Variable | Availability | Why |
|---|---|---|
NEXTAUTH_URL | BUILD + RUNTIME | NextAuth validates URL at module init during build |
NEXTAUTH_SECRET | BUILD + RUNTIME | NextAuth validates secret at module init during build |
AUTH_TRUST_HOST | BUILD + RUNTIME | Read at module init |
FIREBASE_PROJECT_ID | BUILD + RUNTIME | Database adapter initializes at module import |
FIREBASE_CLIENT_EMAIL | BUILD + RUNTIME | Database adapter initializes at module import |
FIREBASE_PRIVATE_KEY | BUILD + RUNTIME | Database adapter initializes at module import |
GOOGLE_CLIENT_ID | RUNTIME only | Read when NextAuth handles first HTTP request |
GOOGLE_CLIENT_SECRET | RUNTIME only | Read when NextAuth handles first HTTP request |
EMAIL_* | RUNTIME only | Used when sending magic link emails |
STRIPE_* | RUNTIME only | Used in API routes at request time |
Why this matters
When a secret has BUILD availability, Cloud Build must have roles/secretmanager.secretAccessor IAM on that secret. If you mark Google OAuth secrets as BUILD without the IAM binding, the build fails:
Error resolving secret version with name=projects/myproject/secrets/myapp-google-client-id/versions/latest
This is confusing because the error looks like a missing secret, but the secret exists — it is an IAM permission issue that only manifests because of the unnecessary BUILD flag.
Rule of thumb: If the code only reads the variable inside an API route handler or a callback function (not at module top-level), it is RUNTIME only.
Failure Mode 3: IAM Bindings on Secrets
There are three ways to create secrets for Firebase App Hosting, and they have different IAM consequences:
Method 1: firebase apphosting:secrets:set (recommended)
firebase apphosting:secrets:set myapp-google-client-id \
--project myproject \
--backend my-backend
This creates the secret AND automatically grants secretAccessor to all required service accounts:
- Cloud Build SA (
{project-number}@cloudbuild.gserviceaccount.com) - App Hosting managed SA (
service-{project-number}@gcp-sa-firebaseapphosting.iam.gserviceaccount.com) - App Hosting compute SA (
firebase-app-hosting-compute@{project}.iam.gserviceaccount.com)
Method 2: gcloud secrets create (manual IAM required)
echo -n "my-client-id" | gcloud secrets create myapp-google-client-id \
--data-file=- --project=myproject
This creates the secret but does NOT grant any IAM bindings. You must then run:
firebase apphosting:secrets:grantaccess myapp-google-client-id \
--project myproject --backend my-backend
If you skip this step, the build will fail with a “misconfigured secret” error, even if the secret value is correct.
Method 3: gcloud secrets add-iam-policy-binding (incomplete)
gcloud secrets add-iam-policy-binding myapp-google-client-id \
--project=myproject \
--member="serviceAccount:{number}@cloudbuild.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
This grants access to ONE service account. But Firebase App Hosting’s orchestration layer checks secrets through multiple service accounts before the build even starts. If you only grant access to the Cloud Build SA but not the App Hosting managed SA, the build still fails.
Always use firebase apphosting:secrets:grantaccess to ensure all three SAs get access. Manually binding one or two is not enough.
Failure Mode 4: Duplicate / Orphaned Secrets
When iterating on a setup, you might end up with multiple secrets for the same purpose:
myapp-google-id # v1: created first, has real credentials
myapp-google-client-id # v2: created with correct naming, has placeholders
If apphosting.yaml references myapp-google-client-id but the real values are in myapp-google-id, your app gets placeholder strings at runtime. The Google OAuth provider sees "placeholder-google-client-id" as a truthy value, so it initializes — but then fails at Google’s end with an invalid client error.
Prevention
Adopt a strict naming convention and stick to it:
{app-prefix}-google-client-id
{app-prefix}-google-client-secret
Where {app-prefix} matches the app’s other secrets (e.g., myapp-nextauth-secret, myapp-firebase-private-key). The -client- infix distinguishes from other google-related secrets you might add later.
The Complete Correct Setup
Here is the full reference for one app, end to end.
1. Create OAuth Client ID in GCP Console
- Go to APIs & Services > Credentials in your project
- Create Credentials > OAuth client ID
- Application type: Web application
- Authorized redirect URIs:
https://app.yourdomain.com/api/auth/callback/googlehttp://localhost:3000/api/auth/callback/google(for local dev)
- Authorized JavaScript origins: leave empty
2. Store credentials in Secret Manager
firebase apphosting:secrets:set myapp-google-client-id \
--project myproject --backend my-backend
firebase apphosting:secrets:set myapp-google-client-secret \
--project myproject --backend my-backend
3. Reference in apphosting.yaml
env:
# Google OAuth (RUNTIME-only: consumed at first HTTP request, not during build)
- variable: GOOGLE_CLIENT_ID
secret: myapp-google-client-id
availability:
- RUNTIME
- variable: GOOGLE_CLIENT_SECRET
secret: myapp-google-client-secret
availability:
- RUNTIME
4. Read in NextAuth config
// lib/auth.ts
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
// Google OAuth (optional -- if env vars are missing, provider is not added)
...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
? [
Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
]
: []),
],
// ... rest of config
});
Debugging Checklist
When Google OAuth is not working on a deployed Firebase App Hosting app:
1. Verify the variable name chain
# What does apphosting.yaml say?
grep -A2 "GOOGLE_CLIENT" apps/myapp/apphosting.yaml
# What does the code read?
grep "GOOGLE_CLIENT" apps/myapp/lib/auth.ts
These must match exactly.
2. Verify the secret has a real value
gcloud secrets versions access latest \
--secret=myapp-google-client-id \
--project=myproject
If it says placeholder-google-client-id, the secret was created but never populated.
3. Verify IAM bindings
firebase apphosting:secrets:grantaccess myapp-google-client-id \
--project myproject --backend my-backend
This is idempotent — safe to run even if already granted.
4. Check availability flags
Google OAuth secrets should be RUNTIME only. If they are marked BUILD and the build fails with a secret resolution error, either:
- Remove
BUILDfrom the availability list, or - Run
firebase apphosting:secrets:grantaccessto ensure Cloud Build has access
5. Verify the OAuth Client ID in GCP Console
The redirect URI must include /api/auth/callback/google (NextAuth v5’s default callback path). If it is wrong, Google will reject the OAuth flow with a redirect_uri_mismatch error.
Multi-App Monorepo Considerations
If you deploy multiple Next.js apps from the same monorepo (each with its own Firebase project and apphosting.yaml), standardise aggressively:
- Same variable names everywhere: Every app reads
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET. No variations. - Same availability flags: Google OAuth is always
RUNTIMEonly. No exceptions. - Consistent secret naming:
{app-prefix}-google-client-idacross all projects. - One shared auth package: The factory function that creates the NextAuth config should live in a shared package, not be duplicated per app. This ensures every app reads the same env var names.
- Audit periodically: Drift happens. A quick
grep -r "AUTH_GOOGLE\|GOOGLE_CLIENT" apps/*/apphosting.yamlcatches mismatches before they reach production.
Key Takeaways
- Variable names in
apphosting.yamlmust matchprocess.env.*in code exactly. This is not validated at build time. - Google OAuth secrets should be
RUNTIMEonly. Marking themBUILDcreates unnecessary IAM requirements. - Use
firebase apphosting:secrets:grantaccess, not rawgcloudIAM bindings. The Firebase CLI grants access to all three required service accounts. - NextAuth silently skips providers with missing credentials. There is no error, no warning, no log line. Test Google sign-in explicitly on every deploy.
- Adopt a strict secret naming convention and never deviate. Duplicate secrets with subtly different names are the hardest bugs to find.