How can one build artifact be promoted through staging and production when client configuration is frozen into the bundle at build time?
answer
- inlining makes the artifact environment-specific
- promote the artifact you tested
- read privately, deliver a chosen subset
- enumerate keys, never the whole environment
- payload and folding are the price
basics
~20 sKeep environment-varying values off the inlined path. Inline only what is identical everywhere, read the rest privately on the server per request, and hand the client an explicit, author-chosen subset at render so one artifact stays environment-neutral.
solid answer
~40 sBuild-time inlining and artifact promotion pull in opposite directions: the moment a value that differs per environment is inlined, the bundle is a *staging* bundle and shipping it to production ships staging's values. Two coherent answers exist. **Build once per environment** — simple, and the artifact you tested is not the artifact you release. **Build once and deliver the varying values at render** — the server reads them privately per request and the page carries a small, explicitly enumerated public subset that client code reads instead of an inlined constant. The second keeps promotion honest but costs a few bytes in every response, gives up constant folding on those values, and demands discipline: enumerate the keys you expose, never hand over the whole environment. Most teams mix the two.
code
pseudocode · 13 lines# server, while rendering the response
private_key = env("UPSTREAM_API_KEY") # stays here, never crosses
public_config = {
"apiBaseUrl": env("API_BASE_URL"), # differs per environment
"tenant": env("TENANT_ID"),
"newCheckout": env("FLAG_NEW_CHECKOUT") == "on"
}
render(page, embed = public_config) # exactly these three keys cross
# never do this: it hands over everything the process was given
# render(page, embed = env)go deeper
Grasp the tension first: a value baked in at build time belongs to the environment that built it, so the same file cannot be correct everywhere unless the value is identical everywhere.
Explain both routes — a build per environment, or a private server read whose result is delivered to the page at render — and what each costs in build time and payload.
Own the discipline: enumerate the keys that cross, keep credentials out of the subset entirely, and add a pipeline check that the promoted artifact carries no environment-varying literal.
Make the split policy. Decide which classes of value may freeze, document where each lives, and weigh testing the exact artifact you release against the payload and optimisation you give up.
## The conflict, stated plainly Two widely held release principles collide here. - **Promote the artifact you tested.** Building separately per environment means production runs bytes no one exercised in staging. - **Configuration differs per environment.** Base URLs, tenant identifiers, flag states and telemetry destinations are rarely identical. Build-time inlining resolves the collision in favour of the second and against the first: a value frozen into a chunk makes that chunk environment-specific, whatever your pipeline calls it. ## Three strategies | Strategy | How client values arrive | What it costs | |---|---|---| | Build per environment | Inlined, correct for that environment | The tested artifact is not the released one; build time multiplies by environments; a promotion is really a rebuild | | One build, values delivered at render | Server reads privately per request, page carries an explicit public subset | Bytes in every response; no constant folding on those values; a code path that must read the delivered value, not a constant | | Hybrid | Build-invariant values inlined, varying ones delivered | Two places to look for configuration, so the split has to be documented | The hybrid is what most mature setups land on, because the sets genuinely differ in kind. Values like a public product identifier, a build stamp or a flag flipped only by release are the same everywhere and inline comfortably. Values that name an environment are the ones that must not. ## How delivery at render works 1. The server handling the request reads the **private** names from its own environment. They are never marked public and never enter the client bundle. 2. Render-time code builds a **small object with keys the author listed**, copying in only the values the browser legitimately needs. 3. That object is embedded in the response the page already sends, and client code reads it from there instead of from an inlined constant. The critical discipline is step 2. Handing over the whole environment object — or a filtered-by-prefix copy computed at runtime — reintroduces the failure the naming rule was designed to prevent, except now there is no build to catch it. Enumerate the keys. ## What it costs, honestly - **Payload.** The subset rides in every document that needs it. Small for a dozen values; a real cost if it grows into a settings dump. - **Optimisation.** A delivered value is a runtime value, so branches around it cannot be folded away and the code for both sides ships. - **Timing.** Client code cannot assume the value exists at module-evaluation time the way a constant does, so read it through one accessor rather than scattering reads. - **A second source of truth.** Two mechanisms now supply client configuration. Without a documented rule for which goes where, values drift between them. ## Deciding which side a value belongs on A workable test, applied in order: 1. **Is it a credential or does possession grant anything?** It is private and server-only; it never crosses by either route. 2. **Does it differ between environments?** If yes, it is delivered at render. If no, it can be inlined. 3. **Does it need to change without a deploy?** Delivered at render, and sourced from wherever the value actually lives rather than from the process environment. 4. **Is it needed before the first response is rendered — during the build itself?** Then it must be inlined, and it must be a value you are content to freeze. ## Two cases the render-time route cannot serve - **Values needed during the build itself.** A page prerendered at build time bakes its HTML then, so a value it renders is fixed then too. Nothing delivered per request reaches a document that is never rendered per request; such routes force a build per environment, or a client-side read after load. - **Values needed before the first byte of client code runs.** Anything consulted while the document is being assembled has to be present on the server at that moment, which is fine, but code that reads it must tolerate the value arriving with the page rather than existing as a module-level constant. ## The failure mode to watch for The subtle one is a value that *used* to be identical across environments and quietly stopped being so. It was inlined years ago, nobody revisited it, and it now carries staging's value into production for one route whose chunk was not rebuilt in the same window. The defence is mechanical: a pipeline check that the emitted client output contains no value belonging to a name classified as environment-varying, run on the artifact that is actually promoted.
- Why is filtering the environment by the public prefix at render time a bad substitute for listing the keys?Because it delegates the decision to whoever names the next variable, and it does so at runtime where no build check can see it. A single misnamed value is then published by a mechanism nobody reviews. An explicit list makes every exposure a reviewable line of code and keeps the set small enough to reason about.
- What do you lose in the built output by delivering a feature flag at render instead of inlining it?Constant folding. An inlined flag lets the optimiser delete the branch it disproves, so the unused path never ships. A delivered flag is only known at runtime, so both paths are in the bundle and the flag merely selects one. That is usually an acceptable trade for being able to flip it without a build, but it is a real byte cost.
- When is building once per environment the better choice?When the environments are few and stable, when the build is cheap, and especially when values must be present during the build itself — a prerendered page that bakes an environment-specific URL into its HTML cannot be fixed by a render-time value on routes that are never rendered per request. Accept that promotion then means rebuilding from the same commit, and pin the commit.
saying these in an interview costs you the question
- Claims a single build is automatically environment-neutral
- Spreads the whole environment into the value handed to the page
- Ignores the payload cost of configuration carried in every response
- Expects constant folding on a value delivered at runtime
- Puts credentials in the delivered subset because the page needs a request made
- Has no documented rule for which values inline and which are delivered