Skip to main content
tutorial Featured

Firebase App Hosting + NextAuth.js: The Google OAuth Configuration That Silently Breaks

A field guide to the subtle configuration mismatches between Firebase App Hosting, GCP Secret Manager, and NextAuth.js that cause Google OAuth to silently fail. Includes the apphosting.yaml availability flag rules nobody documents.

BY Group
February 10, 2026
15 min read

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:

  1. Different people set up the YAML and the auth code
  2. NextAuth’s own documentation uses AUTH_GOOGLE_ID in some examples
  3. The naming convention you chose when creating secrets (myapp-google-id) bleeds into the variable name
  4. 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 build runs)
  • RUNTIME: Injected into the Cloud Run container (when requests are served)

Which variables need which flags

VariableAvailabilityWhy
NEXTAUTH_URLBUILD + RUNTIMENextAuth validates URL at module init during build
NEXTAUTH_SECRETBUILD + RUNTIMENextAuth validates secret at module init during build
AUTH_TRUST_HOSTBUILD + RUNTIMERead at module init
FIREBASE_PROJECT_IDBUILD + RUNTIMEDatabase adapter initializes at module import
FIREBASE_CLIENT_EMAILBUILD + RUNTIMEDatabase adapter initializes at module import
FIREBASE_PRIVATE_KEYBUILD + RUNTIMEDatabase adapter initializes at module import
GOOGLE_CLIENT_IDRUNTIME onlyRead when NextAuth handles first HTTP request
GOOGLE_CLIENT_SECRETRUNTIME onlyRead when NextAuth handles first HTTP request
EMAIL_*RUNTIME onlyUsed when sending magic link emails
STRIPE_*RUNTIME onlyUsed 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:

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

  1. Go to APIs & Services > Credentials in your project
  2. Create Credentials > OAuth client ID
  3. Application type: Web application
  4. Authorized redirect URIs:
    • https://app.yourdomain.com/api/auth/callback/google
    • http://localhost:3000/api/auth/callback/google (for local dev)
  5. 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 BUILD from the availability list, or
  • Run firebase apphosting:secrets:grantaccess to 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:

  1. Same variable names everywhere: Every app reads GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET. No variations.
  2. Same availability flags: Google OAuth is always RUNTIME only. No exceptions.
  3. Consistent secret naming: {app-prefix}-google-client-id across all projects.
  4. 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.
  5. Audit periodically: Drift happens. A quick grep -r "AUTH_GOOGLE\|GOOGLE_CLIENT" apps/*/apphosting.yaml catches mismatches before they reach production.

Key Takeaways

  1. Variable names in apphosting.yaml must match process.env.* in code exactly. This is not validated at build time.
  2. Google OAuth secrets should be RUNTIME only. Marking them BUILD creates unnecessary IAM requirements.
  3. Use firebase apphosting:secrets:grantaccess, not raw gcloud IAM bindings. The Firebase CLI grants access to all three required service accounts.
  4. NextAuth silently skips providers with missing credentials. There is no error, no warning, no log line. Test Google sign-in explicitly on every deploy.
  5. Adopt a strict secret naming convention and never deviate. Duplicate secrets with subtly different names are the hardest bugs to find.
B

BY Group

Software engineering studio building high-quality products with minimal overhead.

Ready to Build Something Great?

Let's discuss your project and bring your ideas to life.

Start a Project

No credit card required • Free forever plan available