Environment & Configuration

Split public from server-only env vars, validate them at boot, and understand when each value is inlined.

Environment handling is where secrets leak and deploys break. Two rules prevent most incidents: know what is public, and fail fast when config is missing.

Public vs server-only

Any variable prefixed NEXT_PUBLIC_ is inlined into the client bundle at build time — it is shipped to every browser, forever, baked into the JS. Everything else stays server-only and is read at runtime. So:

  • API keys, DB URLs, signing secrets → plain names, never NEXT_PUBLIC_.
  • Analytics site ids, public base URLs, feature flags safe to expose → NEXT_PUBLIC_.

Because NEXT_PUBLIC_ values are inlined at build time, changing one requires a rebuild — you cannot swap it at runtime, which surprises people deploying the same image to multiple environments.

Validate at boot, not at use

A missing env var should crash the app on startup with a clear message, not throw a cryptic undefined error deep in a request three days later. Parse process.env through a schema (Zod) once, in a module imported early, and export a typed env object everywhere else. You get autocomplete and a guarantee the value exists.

Example

Example · typescript
// env.ts — validated once, typed everywhere. Import early.
import { z } from 'zod';

const schema = z.object({
  DATABASE_URL: z.string().url(),
  SESSION_SECRET: z.string().min(32),
  // Public — safe to inline into the browser bundle
  NEXT_PUBLIC_APP_URL: z.string().url(),
});

const parsed = schema.safeParse(process.env);
if (!parsed.success) {
  console.error('Invalid environment:', parsed.error.flatten().fieldErrors);
  throw new Error('Missing/invalid environment variables');
}

export const env = parsed.data;

// Usage anywhere on the server:
//   import { env } from '@/env';
//   new Pool({ connectionString: env.DATABASE_URL });
// Secrets NEVER get a NEXT_PUBLIC_ prefix.

When to use it

  • A team validates all required environment variables at startup with Zod so the app fails fast with a clear error if a secret is missing.
  • A developer keeps NEXT_PUBLIC_ variables in .env and secrets in .env.local so the former can be committed safely.
  • A CI pipeline sets production environment variables in the deployment platform, never in checked-in files, keeping secrets out of git.

More examples

Validate env vars with Zod at boot

Parsing process.env through Zod at module load time throws a descriptive error if any variable is missing or malformed.

Example · ts
// lib/env.ts
import 'server-only';
import { z } from 'zod';

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
  NEXT_PUBLIC_APP_URL: z.string().url(),
});

export const env = envSchema.parse(process.env);

NEXT_PUBLIC_ vs server-only vars

NEXT_PUBLIC_ variables are string-replaced in the client bundle at build time — never use this prefix for secrets.

Example · bash
# .env.local
DATABASE_URL=postgres://...         # server only
STRIPE_SECRET_KEY=sk_live_...       # server only
NEXT_PUBLIC_STRIPE_PK=pk_live_...   # inlined in client bundle
NEXT_PUBLIC_APP_URL=https://acme.com # inlined in client bundle

Typed env usage in a component

Importing env from the validated module gives TypeScript types and a guaranteed non-null secret at runtime.

Example · ts
// Typed access via the validated env object
import { env } from '@/lib/env';

export async function getStripeClient() {
  const Stripe = (await import('stripe')).default;
  return new Stripe(env.STRIPE_SECRET_KEY);
}

Discussion

  • Be the first to comment on this lesson.
Environment & Configuration — Next.js | SoundsCode