{"slug":"env-setup-management","title":"Environment Variable Setup and Management for Node.js and Next.js","tags":["env","secrets","deployment","configuration","nextjs"],"agent_summary":"Environment variable setup, validation, and management for Next.js and Node.js projects — covering local dev, preview, and production environments with Zod validation.","trigger_phrases":["environment variables","env setup",".env file","env validation","NEXT_PUBLIC","secrets management","env Zod"],"runnable":true,"markdown":"\n## Overview\n\nStructured environment variable management for Next.js projects. Covers naming conventions, validation at startup, and multi-environment patterns.\n\n## File Hierarchy (Next.js)\n\nFiles are loaded in this priority order (later overrides earlier):\n\n```\n.env                    # Defaults — committed to git\n.env.local              # Local overrides — NEVER committed\n.env.development        # Dev-specific — committed\n.env.development.local  # Dev local — NEVER committed\n.env.production         # Production values — committed (non-secrets only)\n.env.production.local   # Production local — NEVER committed\n```\n\n**Rule:** Never put actual secrets in files that are committed. `.local` files stay off git.\n\n## NEXT_PUBLIC_ Prefix\n\nVariables prefixed `NEXT_PUBLIC_` are inlined into the client bundle at build time. Everything else is server-only.\n\n```bash\n# Server-only (safe for secrets)\nDATABASE_URL=postgresql://...\nSUPABASE_SERVICE_ROLE_KEY=eyJ...\nANTHROPIC_API_KEY=sk-ant-...\n\n# Client-accessible (never put secrets here — visible in browser)\nNEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co\nNEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...\nNEXT_PUBLIC_APP_URL=https://myapp.com\n```\n\n## Zod Validation at Startup (Recommended Pattern)\n\nFail fast if required env vars are missing or malformed:\n\n```typescript\n// src/env.ts\nimport { z } from \"zod\";\n\nconst serverSchema = z.object({\n  NODE_ENV: z.enum([\"development\", \"test\", \"production\"]),\n  DATABASE_URL: z.string().url(),\n  SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),\n  ANTHROPIC_API_KEY: z.string().startsWith(\"sk-ant-\"),\n  OPENAI_API_KEY: z.string().startsWith(\"sk-\"),\n});\n\nconst clientSchema = z.object({\n  NEXT_PUBLIC_SUPABASE_URL: z.string().url(),\n  NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1),\n  NEXT_PUBLIC_APP_URL: z.string().url(),\n});\n\n// Validate server env (runs only on server)\nexport const env = serverSchema.parse({\n  NODE_ENV: process.env.NODE_ENV,\n  DATABASE_URL: process.env.DATABASE_URL,\n  SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY,\n  ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,\n  OPENAI_API_KEY: process.env.OPENAI_API_KEY,\n});\n\n// Validate client env (runs on both)\nexport const clientEnv = clientSchema.parse({\n  NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,\n  NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,\n  NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,\n});\n```\n\nIf validation fails at startup, the app crashes with a clear error listing exactly which variable is missing — not a cryptic runtime failure.\n\n## .env.example Template\n\nAlways maintain `.env.example` with all variables documented but values removed:\n\n```bash\n# Database\nDATABASE_URL=postgresql://user:password@host:5432/dbname\n\n# Supabase\nNEXT_PUBLIC_SUPABASE_URL=https://xxxxxxxxxxxxxxxxxxxx.supabase.co\nNEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... (get from supabase.com dashboard)\nSUPABASE_SERVICE_ROLE_KEY=eyJ... (service role — keep secret)\n\n# Authentication (Clerk)\nNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...\nCLERK_SECRET_KEY=sk_...\n\n# AI\nANTHROPIC_API_KEY=sk-ant-...\nOPENAI_API_KEY=sk-...\n\n# App\nNEXT_PUBLIC_APP_URL=http://localhost:3000\n```\n\nCommit `.env.example`. New devs clone + copy this to `.env.local`, fill in values.\n\n## Vercel Environment Setup\n\n```bash\n# Add to production only\nvercel env add SUPABASE_SERVICE_ROLE_KEY production\n\n# Add to all environments\nvercel env add NEXT_PUBLIC_APP_URL production preview development\n\n# Pull production env to local\nvercel env pull .env.local\n\n# List all env vars\nvercel env ls\n```\n\n## Multi-Environment Pattern\n\n```typescript\n// src/lib/config.ts\nconst configs = {\n  development: {\n    apiUrl: \"http://localhost:3001\",\n    debug: true,\n    logLevel: \"debug\" as const,\n  },\n  production: {\n    apiUrl: process.env.NEXT_PUBLIC_API_URL!,\n    debug: false,\n    logLevel: \"error\" as const,\n  },\n};\n\nexport const config = configs[process.env.NODE_ENV === \"production\" ? \"production\" : \"development\"];\n```\n\n## Security Rules\n\n- `.env.local` and `.env*.local` must be in `.gitignore` — verify before every first commit\n- Never `console.log()` env vars — they end up in logs\n- Never pass entire `process.env` to client components\n- Rotate keys that were ever committed to git — assume compromised\n- Use `z.string().startsWith(\"sk-\")` validation to catch swapped keys early\n","html":"<h2>Overview</h2>\n<p>Structured environment variable management for Next.js projects. Covers naming conventions, validation at startup, and multi-environment patterns.</p>\n<h2>File Hierarchy (Next.js)</h2>\n<p>Files are loaded in this priority order (later overrides earlier):</p>\n<pre><code>.env                    # Defaults — committed to git\n.env.local              # Local overrides — NEVER committed\n.env.development        # Dev-specific — committed\n.env.development.local  # Dev local — NEVER committed\n.env.production         # Production values — committed (non-secrets only)\n.env.production.local   # Production local — NEVER committed\n</code></pre>\n<p><strong>Rule:</strong> Never put actual secrets in files that are committed. <code>.local</code> files stay off git.</p>\n<h2>NEXT_PUBLIC_ Prefix</h2>\n<p>Variables prefixed <code>NEXT_PUBLIC_</code> are inlined into the client bundle at build time. Everything else is server-only.</p>\n<pre><code class=\"language-bash\"># Server-only (safe for secrets)\nDATABASE_URL=postgresql://...\nSUPABASE_SERVICE_ROLE_KEY=eyJ...\nANTHROPIC_API_KEY=sk-ant-...\n\n# Client-accessible (never put secrets here — visible in browser)\nNEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co\nNEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...\nNEXT_PUBLIC_APP_URL=https://myapp.com\n</code></pre>\n<h2>Zod Validation at Startup (Recommended Pattern)</h2>\n<p>Fail fast if required env vars are missing or malformed:</p>\n<pre><code class=\"language-typescript\">// src/env.ts\nimport { z } from \"zod\";\n\nconst serverSchema = z.object({\n  NODE_ENV: z.enum([\"development\", \"test\", \"production\"]),\n  DATABASE_URL: z.string().url(),\n  SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),\n  ANTHROPIC_API_KEY: z.string().startsWith(\"sk-ant-\"),\n  OPENAI_API_KEY: z.string().startsWith(\"sk-\"),\n});\n\nconst clientSchema = z.object({\n  NEXT_PUBLIC_SUPABASE_URL: z.string().url(),\n  NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1),\n  NEXT_PUBLIC_APP_URL: z.string().url(),\n});\n\n// Validate server env (runs only on server)\nexport const env = serverSchema.parse({\n  NODE_ENV: process.env.NODE_ENV,\n  DATABASE_URL: process.env.DATABASE_URL,\n  SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY,\n  ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,\n  OPENAI_API_KEY: process.env.OPENAI_API_KEY,\n});\n\n// Validate client env (runs on both)\nexport const clientEnv = clientSchema.parse({\n  NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,\n  NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,\n  NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,\n});\n</code></pre>\n<p>If validation fails at startup, the app crashes with a clear error listing exactly which variable is missing — not a cryptic runtime failure.</p>\n<h2>.env.example Template</h2>\n<p>Always maintain <code>.env.example</code> with all variables documented but values removed:</p>\n<pre><code class=\"language-bash\"># Database\nDATABASE_URL=postgresql://user:password@host:5432/dbname\n\n# Supabase\nNEXT_PUBLIC_SUPABASE_URL=https://xxxxxxxxxxxxxxxxxxxx.supabase.co\nNEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... (get from supabase.com dashboard)\nSUPABASE_SERVICE_ROLE_KEY=eyJ... (service role — keep secret)\n\n# Authentication (Clerk)\nNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...\nCLERK_SECRET_KEY=sk_...\n\n# AI\nANTHROPIC_API_KEY=sk-ant-...\nOPENAI_API_KEY=sk-...\n\n# App\nNEXT_PUBLIC_APP_URL=http://localhost:3000\n</code></pre>\n<p>Commit <code>.env.example</code>. New devs clone + copy this to <code>.env.local</code>, fill in values.</p>\n<h2>Vercel Environment Setup</h2>\n<pre><code class=\"language-bash\"># Add to production only\nvercel env add SUPABASE_SERVICE_ROLE_KEY production\n\n# Add to all environments\nvercel env add NEXT_PUBLIC_APP_URL production preview development\n\n# Pull production env to local\nvercel env pull .env.local\n\n# List all env vars\nvercel env ls\n</code></pre>\n<h2>Multi-Environment Pattern</h2>\n<pre><code class=\"language-typescript\">// src/lib/config.ts\nconst configs = {\n  development: {\n    apiUrl: \"http://localhost:3001\",\n    debug: true,\n    logLevel: \"debug\" as const,\n  },\n  production: {\n    apiUrl: process.env.NEXT_PUBLIC_API_URL!,\n    debug: false,\n    logLevel: \"error\" as const,\n  },\n};\n\nexport const config = configs[process.env.NODE_ENV === \"production\" ? \"production\" : \"development\"];\n</code></pre>\n<h2>Security Rules</h2>\n<ul>\n<li><code>.env.local</code> and <code>.env*.local</code> must be in <code>.gitignore</code> — verify before every first commit</li>\n<li>Never <code>console.log()</code> env vars — they end up in logs</li>\n<li>Never pass entire <code>process.env</code> to client components</li>\n<li>Rotate keys that were ever committed to git — assume compromised</li>\n<li>Use <code>z.string().startsWith(\"sk-\")</code> validation to catch swapped keys early</li>\n</ul>\n"}