# The Environment Contract Is Part of the Product
A postmortem on an auth flag regression in an agent-built app, and the deploy contract that stops stale environments from becoming releases.
Authors: [Omar U. Espejel](/about/)
Published: 2026-05-10
Updated: 2026-05-12
Tags: agents, deployment, infrastructure, env
## TL;DR

- A stale environment shape became deploy input.
- Smoke derived success from that same broken environment.
- Vaults or encrypted files should own secret values; an env contract should own shape and invariants.
- Remote .env files should be artifacts, never source of truth.
- Every deploy should leave a machine-readable attestation.

## The incident

A large product we are building mostly with agents regressed in preview.

The `/api-key` page rendered:

> API key access is not enabled in this environment yet.

That was wrong. The code had not rolled back. The public API still worked. The page obeyed the environment.

Preview had lost the variables that enable self-serve auth: public flags, server flags, OAuth, Better Auth URL, and related config.

So the page rendered the disabled fallback. The bug was not the component. The bug was that a bad environment reached preview and still counted as success.

## The failure loop

The failure had two steps:

1. deploy accepted an old environment shape as valid input,
2. smoke derived expected behavior from that same environment.

The broken environment told smoke: auth is disabled here. Smoke checked that auth looked disabled, and passed.

The loop:

- the source of truth drifted,
- the deploy script treated drift as configuration,
- runtime obeyed the drift,
- the test oracle used drift to define success.

That is a release-boundary bug.

## The contract we should use

[The Twelve-Factor App](https://12factor.net/config) is still right: deploy config does not belong in code. Hostnames, database URLs, credentials, and flags vary by deploy.

But separation is not enough. Configuration is an interface. It needs a type, an owner, and a test oracle.

A secret store should not decide whether a deploy is valid. A checked-in contract should.

The contract should say:

- preview self-serve auth is required,
- `NEXT_PUBLIC_SELF_SERVE_AUTH_ENABLED` must be `true`,
- `SELF_SERVE_AUTH_ENABLED` must be `true`,
- the Better Auth URL must match the preview host,
- OAuth can only be enabled if credentials are present,
- API key peppers must match across services,
- required public flags must be present at build time,
- deploy cannot complete without a source proof and smoke result.

[Varlock](https://varlock.dev/guides/schema/) gets the important part right: commit the variable schema. Names, types, defaults, comments, and invariants should be reviewed like code.

The value source is a separate choice. [1Password service accounts](https://developer.1password.com/docs/service-accounts/use-with-1password-cli/), [Infisical machine identities](https://infisical.com/docs/documentation/platform/identities/universal-auth), and [Doppler service tokens](https://docs.doppler.com/docs/service-tokens) give machines scoped access to secrets. [SOPS](https://github.com/getsops/sops) plus [age](https://github.com/FiloSottile/age) gives a different tradeoff: encrypted env files in the repo, decryptable only by approved keys.

Any of those can work. None of them should be the release policy.

The split is:

```text
secret backend = values
env contract   = allowed release shape
deploy runner  = decrypt / render / enforce
smoke test     = contract-derived behavior check
```

The rule is simple:

> A remote `.env` file is an output artifact. It is never the source of truth.

If deploy cannot render and validate the desired environment, it should fail before build.

No fallback.

No "use whatever is already on the host."

No smoke test that asks the broken environment what success means.

The Hacker News fights about [secrets in env vars](https://news.ycombinator.com/item?id=41768457), [encrypted shell secrets](https://news.ycombinator.com/item?id=43721228), and [client/server env leaks](https://news.ycombinator.com/item?id=47851634) all circle the same practical problem: lifetime, blast radius, and auditability. Values and policy need different owners.

For a small agent-heavy team, the cleanest deploy foundation is SOPS + age for preview autonomy:

- the encrypted env file is committed,
- the age private key belongs to the deploy runner,
- agents call an allowlisted deploy command,
- the runner decrypts internally and never prints plaintext,
- deploy succeeds only after contract validation, smoke, and attestation.

1Password can still be the human vault. It should not be the deploy control plane unless its service account paths, item names, and invariants are also governed by the same contract.

## Why this matters more for agent-built software

This product is built mostly by agents.

Humans can sometimes survive on memory: preview auth should be enabled; do not overwrite that env file; check this route after deploy; that flag is build-time.

An agent-built system should assume memory is not a control plane.

Agents resume threads, read files, and execute scripts. They can follow a contract. They should not infer deployment truth from stale chat or remote shell state.

So release should be written for machines:

- a schema,
- a redacted env fingerprint,
- a source proof,
- a deploy attestation,
- and a required gate list.

[SLSA provenance](https://slsa.dev/spec/v1.2/provenance) describes where an artifact came from and how it was produced. Deploys need the same habit: leave enough evidence for the next agent.

The attestation is not bureaucracy. It is the comparison object.

If yesterday's preview had schema hash `A`, image digest `B`, source revision `C`, env fingerprint `D`, and an auth-enabled smoke result, today's preview should explain every difference. Otherwise, no success claim.

## The implementation shape

Start with preview:

1. Add a committed env contract for preview.
2. Copy the known-good env into an encrypted SOPS file. Do not move or delete the old source.
3. Keep the age private key with the deploy runner, not in every agent shell.
4. Render the deploy env from the approved source.
5. Validate the rendered env before build.
6. Build the image with the env contract hash as metadata.
7. Deploy only after validation passes and source proof is written.
8. Run smoke tests whose expectations come from the contract.
9. Write a redacted deploy attestation.
10. Fail the sync if any proof is missing.

The first version can be JSON plus a validator. Move to Varlock or another schema tool later. Do not wait for the perfect framework before removing remote-env fallback.

The important part is the direction of authority:

```text
encrypted values + contract -> render -> validate -> build -> deploy -> smoke -> attestation
```

Not:

```text
remote env -> deploy -> runtime says what to test
```

## The takeaway

The environment contract is part of the product.

If a feature depends on build-time flags, OAuth, hostnames, peppers, API keys, or public runtime flags, those facts are not operational trivia.

They are release facts.

They need the same treatment as code:

- versioned,
- reviewed,
- validated,
- tested,
- and proven after deploy.

For agent-built software, this is not optional hardening. It is the interface between agents and production.
