Verified against Claude Code · 2026-07-30
Set up environment variables so a secret never ends up in the client bundle
Designs a NEXT_PUBLIC_ vs server-only variable split across local, staging, and production, catching the specific mistake of a secret prefixed for client exposure by habit.
The prompt
Ready to copy — highlighted parts are example details you can swap.
You are designing the environment variable setup for a Next.js app across its local, staging, and production environments, with the goal of making it structurally difficult for a secret to end up exposed in the client bundle by accident. VARIABLES NEEDED DATABASE_URL, STRIPE_SECRET_KEY, NEXT_PUBLIC_ANALYTICS_ID, SESSION_SECRET, NEXT_PUBLIC_API_BASE_URL ENVIRONMENTS local development, a staging environment on a preview branch, and production DEPLOYMENT PLATFORM Vercel for staging and production; local development runs with next dev CURRENT SETUP A single .env file with all variables, and STRIPE_SECRET_KEY is currently prefixed NEXT_PUBLIC_ because an early prototype needed it in a client component that has since been refactored to a Server Action DESIGN RULES Classify every variable as either genuinely needed in the browser or server-only, and treat that classification as the single fact that decides its name — a variable prefixed with NEXT_PUBLIC_ gets inlined into the client JavaScript bundle at build time, which means it is not a runtime secret in any environment, no matter how sensitive the value or how tightly the deployment platform's dashboard access is restricted, and prefixing an API secret key with NEXT_PUBLIC_ out of habit or because the client code happens to need some related, genuinely public value is a real leak, not a naming inconsistency. Never let a server-only variable get read from a Client Component, even indirectly through a shared config module imported by both server and client code — a config file that reads process.env.DATABASE_URL and exports it as a named constant will bundle that value into the client if any client-rendered code imports from the same file, even code that never actually touches the DATABASE_URL export; keep server-only config in a module that a Client Component file cannot import at all, not just one where the specific export happens to go unused there. For each of the stated environments, specify whether the platform reads env vars at build time or at runtime, since this materially changes the deployment story: a variable baked in at build time requires a rebuild to change, while a runtime-read variable can be updated by changing the platform's configuration and restarting the app, and mixing up which category a given variable actually falls into is a common cause of "I changed the environment variable but nothing changed" reports. Use a single, checked-in .env.example listing every variable name with a placeholder or description but no real value, so a new environment's setup is copy-and-fill rather than reverse-engineered from whichever teammate remembers what's needed; never commit an actual .env file with real values to version control regardless of how private the repository is. State explicitly which variables differ in value across the three environments and which are identical, since a variable that's identical everywhere except accidentally in one environment is a specific and common source of an environment-specific bug that looks like application logic but is actually a config mismatch. OUTPUT FORMAT A table: Variable name | Client-exposed or server-only | Build-time or runtime | Differs across environments (yes/no, and how) | Where it's actually read in the codebase. Then the .env.example file content, and one paragraph flagging any variable in the current setup that's misclassified today.
Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
The NEXT_PUBLIC_ prefix is not a permissions setting or an access-control decision, it's a build-time instruction to inline that value directly into the JavaScript bundle shipped to every visitor's browser, which means the moment a secret carries that prefix, it is exposed in the same sense as if it had been printed on the page — restricting who can view the deployment platform's dashboard afterward does nothing to undo that, since the value is already sitting in a script tag on every page load, readable by anyone who opens their browser's developer tools. The transitive-import warning targets a specific and genuinely surprising bundling behavior: a shared config module that reads a server-only variable and re-exports it can leak that value into the client bundle if any client-rendered file imports from that same module at all, even for a completely unrelated export, because the bundler doesn't understand that one export is unused in that particular consumer — it only sees that the module, as a whole, was pulled into a client bundle, and it includes what's needed to make every export in that module work, not just the one the client code actually references. Distinguishing build-time from runtime variable resolution matters because it determines what "changing a config value" actually requires in each environment — a variable read only at build time genuinely cannot be updated by editing a platform dashboard and restarting the app, since the value is already frozen into the built output, and treating it as if a restart would pick up the change leads to a debugging session spent looking for a bug in application logic that's actually a deployment-pipeline misunderstanding. Requiring an explicit statement of which variables differ across environments, rather than leaving that implicit in three separate .env files, surfaces the specific and common failure where a value is supposed to be identical everywhere but silently isn't in one environment — a typo in a staging API base URL, for instance — which otherwise only gets discovered when staging behaves subtly differently from production for a reason nobody thought to check because nothing about the setup flagged that these two values were even supposed to match.
What you get back
STRIPE_SECRET_KEY | server-only | runtime (read inside a Server Action, never bundled) | identical across staging/production, different in local (test-mode key) | lib/payments.ts, called only from 'use server' actions NEXT_PUBLIC_API_BASE_URL | client-exposed | build-time (inlined at build) | differs per environment (staging vs production API host) | read directly in app/providers/api-client.ts, a Client Component
Flag: STRIPE_SECRET_KEY is currently prefixed NEXT_PUBLIC_ in the existing .env file, a leftover from a client component that has since been removed — this is exposing a live Stripe secret key in the production JavaScript bundle right now and should be renamed and rotated immediately, not just renamed on the next deploy.
Verified against
Claude Code Sonnet 4.6 · 2026-07-30
Cursor Cursor 2.1 · 2026-08-06
Changelog
- 2026-07-30 — Initial publish, verified against Claude Code (Sonnet 4.6) and Cursor 2.1, including the transitive-import bundling check on a Next.js 16 app.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
