The last 10% of auth work — getting it from “works on my machine” to “works on the public internet” — tends to sprawl into its own mini-project if you haven’t prepared for it. Here’s what actually changes between localhost and production, and how to make the leap boring.
1. Redirect URIs
The IdP (Google, GitHub, whatever) has a hardcoded list of allowed redirect URIs. In dev you probably set http://localhost:3000/auth/callback. In prod you need https://yourapp.com/auth/callback.
Two options:
- Add both to the IdP’s redirect URI list (easiest, but dev credentials shouldn’t have prod URIs)
- Use separate OAuth apps for dev and prod (cleaner, more annoying)
I recommend separate apps. Your prod secrets never leave prod; your dev secrets never leave dev. Two sets of env vars:
# .env.local
GOOGLE_CLIENT_ID=dev_xxx
GOOGLE_CLIENT_SECRET=dev_yyy
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/callback
# .env.production
GOOGLE_CLIENT_ID=prod_zzz
GOOGLE_CLIENT_SECRET=prod_aaa
GOOGLE_REDIRECT_URI=https://yourapp.com/auth/callback
2. Cookie flags
Development often runs on HTTP. Production always runs on HTTPS. That matters for cookies:
res.cookies.set('session', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production', // must be true in prod, false in dev
sameSite: 'lax',
path: '/',
})
If you set secure: true in dev on HTTP, the cookie won’t be sent and you’ll spend hours debugging “why does sign-in work but the session doesn’t persist?”
If you set secure: false in prod, you’ve created a session cookie that can be intercepted on any wifi network. Don’t.
3. Base URLs
Hardcoded http://localhost:3000 will haunt you. Use environment-derived base URLs:
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000'
For emails with reset links or magic links, this is the line that breaks if you forget. The user gets an email with a localhost link in it, and you have to reissue.
4. CORS
If your frontend is on app.yourapp.com and your API is on api.yourapp.com, cookies need SameSite=None; Secure and CORS configured with credentials: 'include'. In dev, if everything’s on localhost, CORS is invisible.
The test: deploy to a staging environment with separate frontend and API subdomains. If anything breaks, it’s CORS or cookie-domain related.
5. Session signing keys
Rotate the session signing key between dev and prod. Obviously. But also: rotate it when you deploy prod, so existing dev sessions don’t somehow become valid in prod.
The PR checklist
Before merging a PR that goes from localhost to production:
- Separate OAuth app for prod with correct redirect URI
-
secure: truecookies in production -
BASE_URLor equivalent env var for email links - CORS configured if frontend and API are on different subdomains
- Session signing key rotated
- Smoke test: sign in on prod, sign out, sign in again
If any of those are missed, expect a bug report within hours.