Abstract view of modern architecture with geometric shapes and glass facade. Hero image credit: Photo by ian .R on Pexels

Part 16 of the Nuxt and .NET series.

Part 2 said that the same build artefact runs in every environment and that the differences live in configuration. This article is about what that actually takes — because the failure mode it introduces is a release that builds successfully, deploys successfully, reports success, and runs with a setting nobody intended.

Names, values and topology below are illustrative. A note on vocabulary before anything else: this article uses deployment revision for an immutable snapshot of a deployed application — its image plus its configuration — which is a platform concept and has nothing to do with the document revisions in the running example.

Table of Contents

A Green Release With the Wrong Setting

The document application has a setting that enables the shared result-set cache from Part 9. It should be on in production and off in preview environments, where editors need to see content changes immediately.

Now consider the ways that can go wrong without anything turning red. The variable is defined for production and forgotten for the preview overlay, so previews inherit a value that makes editorial review confusing. Or it is defined in the pipeline under one name and read by the application under another, so the application silently uses its default. Or it is set to the string "false", which is truthy in a naive parse, so the cache is on everywhere and an editor’s ticket says “preview shows old content.”

None of those is a build error. Each of them is a configuration decision that existed in two places and matched in neither — the same problem Part 1 described about data contracts, relocated into deployment. So the question here is the same one: which of these agreements can a pipeline check, and which does it merely transport?

Three Environment Classes and What They Isolate

Class Purpose What is actually isolated
Preview Review one proposed change Dedicated application instances and cache; the platform, network and identities may be shared
Integration Exercise changes together A shared deployment with controlled updates
Production Serve real traffic Separately managed infrastructure and release policy

The second column is where the useful precision lives. A preview deployment is usually not a private copy of the world:

flowchart TB subgraph SH["Shared preview platform"] subgraph PA["Preview A"] P1["Proxy"] --> N1["Nuxt"] --> A1["API"] N1 --> C1["Dedicated cache"] end subgraph PB["Preview B"] P2["Proxy"] --> N2["Nuxt"] --> A2["API"] N2 --> C2["Dedicated cache"] end A1 --> DS["Shared downstream service"] A2 --> DS end subgraph PR["Separately managed production"] P3["Proxy"] --> N3["Nuxt"] --> A3["API"] N3 --> C3["Managed cache"] end

A dedicated cache per preview prevents key collisions between two reviews — worth having, given how much of this series depends on cache identity. It does not isolate a shared database, and it does not give each preview its own share of a downstream rate limit, so two active previews plus an integration run can exhaust a quota that production also depends on.

Two operational notes that are easy to get wrong. Previews are created per pull request rather than per push, which keeps the number bounded — though the effective trigger also depends on the repository provider and branch policies, so it is worth verifying rather than assuming. And cleanup is a reconciliation job, not a side effect of deleting a branch: derive the set of deployments that should exist, compare it with what does exist, and remove only confirmed orphans. That requires the naming rules used for provisioning and cleanup to be identical, which is exactly the kind of agreement that drifts.

Base Configuration Plus an Environment Overlay

The model is deliberately small: one base file, one optional overlay per environment, then reference resolution.

values.yaml
    → values.{environment}.yaml, if present
    → resolve {{ configuration.references }}
    → generate deployment artefacts

The merge semantics are the part to internalize, because they are where surprises come from:

  • objects merge recursively — an overlay adds or changes keys without restating the whole tree;
  • arrays, scalars and explicit null replace the base value. An array override is not an append, which people expect roughly half the time;
  • an explicit null is useful precisely because it removes: a preview can null out an inherited secret mapping in order to supply its own.

References resolve after merging. A value consisting entirely of a reference keeps the resolved type, so a boolean stays a boolean; a reference embedded in a longer string becomes text, which is usually what you want for connection strings and never what you want for a flag. Missing references and reference cycles must fail generation — partially substituted output that looks plausible is worse than no output.

Following One Setting Into a Manifest

Base configuration:

nodes:
  spa:
    env:
      NUXT_DOCUMENT_CACHE_ENABLED: false

Production overlay:

nodes:
  spa:
    env:
      NUXT_DOCUMENT_CACHE_ENABLED: true

Generated manifest entry:

{
  "name": "NUXT_DOCUMENT_CACHE_ENABLED",
  "value": "true"
}

Notice the quotation marks in the last step: container environment variables are strings, whatever the YAML said. So the application still needs a runtime configuration property with a matching name and a parse that treats "false" as false. The generator transports a value; it cannot infer what a new variable is supposed to mean, and this is the exact seam where the third failure from the opening section lives.

What generation does buy is that one source produces every artefact that mentions the setting — application manifests, infrastructure parameter files, pipeline variables, and a schema for editor completion — so those artefacts cannot disagree with each other. And a reference validator can check that every pipeline variable a manifest refers to is actually defined, which catches the missing wiring failure.

Be clear about the limit: these checks catch names that do not resolve. They do not catch a valid-looking endpoint pointing at the wrong environment, which is why the release verification later in this article matters.

Who Owns Which Resource

Owner Examples Typical change
Infrastructure deployment Container environment, network, identities, monitoring resources, managed cache Platform change
Application deployment Image reference, environment variables, probes, scaling rules Release change
Secret management Secret values, access grants, rotation Credential lifecycle

The point of the split is blast radius: an ordinary release changes the middle row only, so it cannot accidentally reconfigure a network. It is not a claim that infrastructure changes are safe or that application manifests are harmless — both need review, and anything touching a shared resource needs a compatibility plan, because the shared resource is shared with production.

Secret References Are a Chain

A secret in a container environment variable is usually not the secret. It is a reference to an application-level secret, which is itself a reference into a vault, resolved using a managed identity that must have permission:

flowchart LR ENV["Environment entry<br/>secretRef"] --> APPSEC["Application secret"] APPSEC --> VAULT["Secret in the vault"] ID["Managed identity with access"] --> VAULT VAULT --> RUN["Value supplied to the container"]

Every link can break independently, and the interesting failures are at the ends. Provision the identity and its permissions separately from the release, and verify that the deployed workload can actually resolve the reference — a manifest that references a secret it cannot read starts and then fails in a way that looks like an application bug.

Three consequences worth planning for. A reference protects configuration storage; it does nothing about application code logging the resolved value, so the redaction discipline from Part 15 still applies. Rotation needs an explicit procedure, because changing a vault value does not instantly update every running process — something has to restart or re-resolve, and “we rotated it” and “everything uses the new one” are different states. And removal is two-staged: because retained deployment revisions still reference the application secret, stop mapping it into new revisions first and delete the definition only when no retained revision needs it. Skipping that ordering breaks the release you were keeping for rollback, which you discover at the worst possible moment.

Values Known Only at Deployment Time

Some values cannot exist when configuration is generated: the image tag being deployed, the environment’s assigned domain, monitoring connection details from freshly provisioned resources. Those get explicit placeholders that are substituted during deployment.

flowchart LR SRC["Configuration sources"] --> GEN["Generator"] GEN --> MAN["Manifest with placeholders"] IMG["Built image identity"] --> SUB["Deployment substitution"] RES["Provisioned resource metadata"] --> SUB MAN --> SUB SUB --> VAL["Validate the concrete manifest"] VAL --> APP["Apply the release"]

Substitute structurally — operate on the parsed JSON rather than running text replacement over a manifest — and verify before applying that no required placeholder survived. An unsubstituted placeholder that reaches a running container becomes a literal string in a URL, which fails in a confusing place rather than at the step that should have caught it.

Keep an auditable association between the source commit, the image identity and the generated configuration. When something is wrong in production, “which configuration is this instance running?” should be answerable from records, not by inspection.

Deploy Verify Switch

Now the mechanism Part 2 deferred. A deployment revision is an immutable snapshot of a deployed application: this image with this configuration. Creating one does not send it live traffic, and each one is reachable at its own internal address — which is what makes verification before cutover possible.

For shared environments the candidate release therefore consists of several revisions that must talk to each other rather than to the current live versions, addressed by revision-specific internal hostnames:

flowchart TB D["Deploy candidate revisions:<br/>API, then Nuxt, then proxy"] --> R{"All revisions ready?"} R -->|No| STOP["Stop; live traffic unchanged"] R -->|Yes| S["Exercise the candidate proxy URL"] S --> V{"Checks pass?"} V -->|No| STOP V -->|Yes| SW["Switch proxy traffic to the candidate"] SW --> IR["Update internal default routes"] IR --> K["Retain the previous release for rollback"]

The user-facing cutover happens at the proxy; internal default routes are updated afterwards. These are separate control-plane operations, not one transaction, so a failure between them leaves a state someone has to understand — which is an argument for making the sequence explicit in the pipeline rather than implicit in a deployment tool’s defaults.

The verification step deserves a blunt caveat, because this is where release gates are routinely weaker than they appear. A smoke check that fetches the candidate URL and accepts any successful or redirect response proves that a process is listening. If the application redirects unauthenticated requests to a sign-in page, that check passes while rendering is broken, the gateway is misconfigured, and the API is unreachable. A gate worth the name asserts expected content on a representative page and exercises one real application operation against the candidate. If that stronger check is planned rather than implemented, it should be described that way.

What Revision Routing Does Not Isolate

Revision routing isolates application versions. Four things it does not isolate, all of which have caused bad releases:

  • Shared data. Both revisions read and write the same database. A schema migration is live the moment it is applied, for the old revision too, which is why migrations need to be backward-compatible with the release you intend to roll back to.
  • Cache formats. A candidate that writes a new shape into the shared cache leaves entries the previous release cannot read. Version the cache keys — Part 3’s mechanism — so a rollback does not consume the new revision’s data.
  • External side effects. Messages published, emails sent, webhooks delivered by the candidate during verification are not undone by switching traffic back.
  • Browser clients from the previous release. Someone is holding a bundle from the old version and still sending its operations, which is Part 5’s compatibility point stated operationally: a schema or endpoint change that passes today’s checks can break yesterday’s client for as long as those sessions last.

Rollback and Its Limits

Retaining the previous revision means rollback does not require a rebuild, which is a genuine improvement over the alternative. It is not instantaneous recovery: the retained revision may need reactivation, it has to pass readiness checks, and routing changes take time to propagate. Select the rollback target explicitly by identity — assuming the most recent non-current revision is the right one is how you roll back to a broken release from two hours earlier.

For the initial migration, an edge gateway can switch between the old and new systems without changing public DNS, which makes the cutover reversible in minutes rather than hours. The same qualifications apply: configuration propagation takes time, cached responses at the edge outlive the switch, the origin needs to be warm, and in-flight requests are already in flight. Rehearse both directions, and keep the old system compatible with any shared-data changes the new one has made — otherwise the rollback path exists on paper only.

The Release Checks Worth Having

Before switching traffic: every configuration reference resolves, the workload can read the secrets it maps, no placeholders survived substitution, probes report the meaning you intended, the candidate serves expected content and completes one real operation, and the rollback target is identified and known to work.

After switching: watch user-facing errors and latency, not the deployment command’s exit code. A successful deployment means the platform accepted your manifest, which is a statement about the platform.

The rule this article leaves you with is the same one that opened it, applied to infrastructure: for each setting that differs between environments, name the single file that defines the difference and the check that fails if it is wrong. A setting whose correctness depends on someone remembering which environment they are looking at is not configured — it is merely deployed.

What’s Next


Munir Husseini is a software architect specializing in full-stack TypeScript, .NET, and cloud-native architectures.