Nest Authbeta

Environment & Secrets

Required env vars and how to rotate them.

The library reads no env vars itself — you pass everything via NestAuthModule.forRoot({ ... }). But you'll typically source those values from env vars. Here's the recommended set.

.env.example

# Application
NODE_ENV=development
PORT=3000
APP_URL=http://localhost:3000
CORS_ORIGIN=http://localhost:5173
 
# Database
DATABASE_URL=postgres://user:pass@localhost:5432/myapp
 
# Auth — required
JWT_SECRET=                          # 256-bit random string
ACCESS_TOKEN_VALIDITY=15m
REFRESH_TOKEN_VALIDITY=7d
 
# Sessions
SESSION_STORAGE=DATABASE             # or REDIS
REDIS_HOST=                          # only if SESSION_STORAGE=REDIS
REDIS_PORT=6379
REDIS_PASSWORD=
 
# OAuth — only what you enable
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_REDIRECT_URI=
 
FACEBOOK_APP_ID=
FACEBOOK_APP_SECRET=
FACEBOOK_REDIRECT_URI=
 
APPLE_CLIENT_ID=
APPLE_TEAM_ID=
APPLE_KEY_ID=
APPLE_PRIVATE_KEY=                   # PEM, newlines as \n
APPLE_REDIRECT_URI=
 
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_REDIRECT_URI=
 
# Notifications
RESEND_API_KEY=
SENDGRID_API_KEY=
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_FROM_NUMBER=
 
# Admin Console
ADMIN_CONSOLE_SECRET=

Generating JWT_SECRET

node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

Or:

openssl rand -base64 32

256 bits is the floor. Don't reuse the same secret across environments.

Loading order

// load-env.ts
import { config } from 'dotenv';
config({ path: process.env.NODE_ENV === 'test' ? '.env.test' : '.env' });
// main.ts
import './load-env';            // <-- BEFORE everything else
import { NestFactory } from '@nestjs/core';
// …

If import './load-env' isn't the first import, process.env.JWT_SECRET is undefined when NestAuthModule initializes and signed tokens won't validate.

Rotation

Rotating JWT_SECRET invalidates every outstanding access token. Plan around that:

  1. Pre-deploy: cap accessTokenValidity to a low value (5–15 min) so the blast radius is small.
  2. Deploy with the new secret.
  3. Active users see one extra /auth/refresh round trip on their next request — refresh tokens still work because they're stored server-side, not signed-only.

For zero-downtime rotation, support a secondary secret for verify-only:

session: {
  jwt: {
    secret: process.env.JWT_SECRET,                           // signs new tokens
    legacySecrets: [process.env.JWT_SECRET_PREVIOUS],         // verifies old tokens
  },
},

(Implement via the session.jwt.validateToken hook if you need this — the library doesn't ship it out of the box.)

OAuth secret rotation

OAuth client secrets rotate on a different schedule per provider:

  • Google — generate a new client secret, deploy alongside the old, then revoke the old after 15 min.
  • Apple — keys (p8) don't auto-expire, but the JWT used to authenticate to Apple's token endpoint expires every 6 months. The library refreshes it automatically; you only need to rotate when you actively want to.
  • Facebook / GitHub — generate-and-swap, same as Google.

Secrets manager integration

Don't keep .env in production. Pull secrets from:

  • AWS Secrets Manager / SSM Parameter Store
  • HashiCorp Vault
  • 1Password / Doppler / Infisical

The pattern: in load-env.ts, fetch from the manager and stuff into process.env before the rest of the app boots.

What's safe to log

  • Public URLs, app name, port — yes.
  • Public OAuth client IDs — yes.
  • JWT_SECRET, OAuth client secrets, ADMIN_CONSOLE_SECRET, Resend/SendGrid/Twilio API keys — never.

Audit your error reporters (Sentry, Datadog, etc.) and confirm they redact env vars by default.

Per-environment defaults

VarDevProduction
synchronize (TypeORM)truefalse
cookieOptions.securefalse (HTTP localhost)true
mfa.requiredfalsedepends on policy
audit.enabledfalsetrue
debug.enabledtruefalse (turn on per-area when debugging)

Next

On this page