postmortem

A draft gate that looked like an environment check and was not one

2026-07-27 · CAN-103

The defect

Draft library articles were invisible on the preview channel — the one deployment where a draft is actually useful — and visible only under `next dev`.

Why nothing caught it

The gate read `process.env.NODE_ENV !== 'production'`, which looks like an environment check and is not one. `next build` sets `NODE_ENV=production` for every deployment, preview included, so the condition was really "am I running the dev server", written in a way that nobody reviewing it would read that way. No test asserted draft visibility per deployment environment, so the suite was green.

Wrong turns

  • Reasoning about it instead of measuring it. The bug was settled by deploying a preview build and requesting a draft slug, which returned 404 — 'measured, not reasoned'.
  • Treating `NODE_ENV` as a deployment identity at all. It distinguishes build modes, not deployments, and this repo has three deployment environments that all build in production mode.

The fix

Key the gate on `getAppRuntimeEnvironment()`, which is this repo's authority for the question and what every capability matrix already reads. SEC-121 makes it fail CLOSED to PRODUCTION on an unrecognised value, so an unknown deployment hides drafts rather than leaking them.

Why this is the first record on this surface

The decision-records loader you are reading output from uses the same gate, for the same reason, and imports the same function. A record about draft visibility, published by a surface whose own draft visibility depends on the fix, is the cheapest possible demonstration that this is a working notebook rather than a retrospective.

The shape of the mistake

The failure was not the condition being wrong. NODE_ENV !== 'production' is a perfectly good way to ask "am I in a dev server". The failure was that the question it answered and the question it appeared to answer were different, and the gap was invisible at the call site.

That is a recurring class in this codebase, and it is why the constitution's standing rule is that a claim must not outrun the check that supports it. Here the claim — "drafts are hidden in production" — was true. It was just also hiding them in preview, and nothing in the name, the condition, or the test suite would have told you.

What changed structurally

Two things, beyond the one-line condition:

  • A single authority. getAppRuntimeEnvironment() is now the only way any surface asks which deployment it is on, so a future gate cannot invent its own answer.
  • Failing closed. An unrecognised environment resolves to PRODUCTION. The previous behaviour on an unknown value was to show drafts, which is the wrong direction to fail in.

Where this is canonical

src/lib/library/index.tsThe fix and its reasoning live in the doc comment above `draftsVisibleHere()`. This record is a dated snapshot; the code is canonical.