Learn Which Settings Must Differ Between Local Development and Production
AI links open with a title + excerpt (these tools can't fetch the page themselves) — use "Copy full article" to paste the complete text for a fuller summary.
Introduction
This article demonstrates how we handle the settings that must be different on a developer machine and in production.
Most settings are the same value from a different place — a connection string, a secret key. Those are easy. This article is about the small number that cannot be the same value at all, because local development is plain HTTP on localhost and production is HTTPS on a real domain.
Get these wrong and you get the worst kind of bug: the one that works perfectly on your machine.
The Rule
Never write if (isLocal) in your application code.
The moment you do, you have two code paths, and the one you do not run locally is the one that ships. Instead, derive the difference from something the environment already tells you, or keep it entirely in configuration.
Features of this approach
- One code path everywhere.
- No flag a developer can forget to flip.
- The difference is visible in one line, not scattered through handlers.
With the following steps, you can handle the four cases we hit.
- The
Securecookie flag. - The captcha test keys.
- The storage emulator.
- The settings that must never be missing.
The Secure Cookie Flag
A cookie marked Secure is only stored by the browser over HTTPS. In production that is exactly what you want. Locally, the SWA CLI emulator serves plain HTTP on http://localhost:4290.
So a hardcoded secure: true gives you this: the login request returns 200, the browser quietly refuses to store the cookie, and every request after it is a 401. Nothing errors. The login page just bounces you back, forever.
The fix is one line.
secure: !!process.env.WEBSITE_INSTANCE_ID,
WEBSITE_INSTANCE_ID is set by the Azure hosting environment and does not exist on your machine. So it is true in Azure and false locally, with nothing to configure and nothing to remember.
My suggestion: prefer a variable the platform sets over one you set yourself. A variable you set is a variable somebody can set wrongly. NODE_ENV=production on a developer machine is a very common accident; WEBSITE_INSTANCE_ID cannot be one.
The Captcha Test Keys
Cloudflare Turnstile keys are tied to a domain. Point production keys at localhost and the verification fails for every developer, so nobody can log in locally.
Cloudflare publishes a test site key and secret that always pass. They go in api/local.settings.json.
Notice what this is not. We did not add a "skip captcha when local" branch. The widget still renders, the token is still produced, verifyTurnstile still makes the real HTTP call to Cloudflare and still checks the response. Only the answer is always yes.
That matters, because it means the captcha code path is exercised every single time you run the app locally. A bug in the token plumbing shows up on your machine, not in production.
Warning: both keys must be swapped before you deploy — the site key in the Angular component and TURNSTILE_SECRET_KEY in the Application Settings. If you ship the test pair, every bot also passes. Put it on your deploy checklist, because nothing in the code will tell you.
The Storage Emulator
The Functions host itself needs a storage account. Locally that is Azurite, Microsoft's emulator.
"AzureWebJobsStorage": "UseDevelopmentStorage=true"
Start it before the app.
npm run start:azurite
If you forget, the Functions host starts but its health check reports unhealthy, and you get errors that do not obviously say "storage". This one is worth putting in your README, because the error message does not lead you to the cause.
Your own blob code can point at Azurite the same way, through AZURE_STORAGE_CONNECTION_STRING. Same SDK, same code, different connection string.
Be careful here. Ours currently points at a real storage account even locally. That is a deliberate choice for media work, but it means a local upload writes to the live container. Decide which one you want, and know which one you have — the code looks identical either way.
The Settings That Must Never Be Missing
The last case is not a difference, it is a failure. A missing setting should stop the app immediately, not produce a strange error under traffic three hours later.
Every module that reads configuration checks it at load time.
if (!connectionString) {
throw new Error(
'Missing required environment variable: COSMOS_CONNECTION_STRING. ' +
'Set it in local.settings.json for local development, ' +
'or in Azure Static Web Apps Application Settings for production.'
);
}
Three things make this message useful.
It names the variable, so you do not have to guess which of your fifteen settings is the problem. It names both places it might need setting, so the person reading it knows what to do next, not only what went wrong. And it runs at module level, so a misconfigured deployment fails loudly on startup instead of returning 500s from one endpoint.
We use the same wording in every module. Cosmos, Firestore, Blob Storage, the JWT secret, the Turnstile secret. When the message is identical everywhere, you recognise it instantly.
One Thing That Is Not Config
A related trap. api/local.settings.json holds real connection strings and is gitignored. Keep it that way.
The same is true of the .claude/settings.local.json file if you use Claude Code — allowlisted commands can capture a connection string inside them. Ours is gitignored too. Check yours before you push, and before you screenshot your terminal for a blog post.
Conclusion
In this article we learned four settings that must differ between local development and production — the Secure cookie flag, the captcha test keys, the storage emulator, and the fail-fast check for everything else.
The pattern behind all four is the same. Derive the difference from the environment or keep it in configuration, never in an if statement, so there is only ever one code path to test.
Reference
Comments
Be the first to comment.