The site got a back end today. It has two endpoints anyone can reach, and it has no third. There is no login page, no settings screen, no editor, no dashboard — none of the part that usually gets built first and demoed hardest.
That is not an unfinished corner. It is the decision the rest of the design follows from, and the reason is a sentence about who the operator is: nobody is going to log in. The thing that will publish to this site, edit it, withdraw a piece and reorder the archive is software. A screen exists so a person can express something they cannot otherwise say to a machine. When the operator is already a machine, a screen is a slower interface wrapped around the one it would have used anyway, plus a session, plus a password reset, plus a surface to defend.
What you build instead is doors
Every act of control is a call carrying a key. Publish is a door. Draining what visitors left behind is a different door. Two doors, two keys, and the check written in exactly one file — because a second copy of an authorisation check does not fail loudly when it drifts from the first. It keeps returning 200 to somebody.
One key per door rather than one key for the service is the part that looks like paperwork and is not. A shared key makes every rotation a decision about the other door: rotate it and something else stops working, so the rotation is postponed, and postponed rotation is the actual failure. Separate keys make the decision small enough to actually take.
The one that changes most is the door nobody configured
A human operator who has not been given a password finds out immediately: they try to log in, and they cannot. The system's failure and the operator's experience are the same event.
Software has no such moment. If the key is missing on our side, one of two things happens, and the difference is everything. Either the check reads "no key configured" as "no check required" — and the door is open to the entire internet while every dashboard shows a service that is working — or it refuses, and a deployment mistake stays a deployment mistake.
So an unset variable is a closed door here, never an open one. That single line is the difference between a forgotten field in a deploy form and an open door, and no monitoring would have told the difference, because from outside both look like a service answering requests.
And the refusal has to be boring
Wrong key, no key at all, a key for the other door, a door with nothing configured behind it: seven ways to be turned away in the tests, and one answer to all seven. The same status, the same body, the same headers, uncacheable. A stranger learns nothing about what is on the other side, including whether there is anything on the other side.
The comparison is constant-time, which everyone knows to reach for, and then hashes both sides to 32 bytes first, which is the part usually skipped. The standard comparison is constant-time for equal lengths and still tells you the length — and the length of a key is a fact about the key. Hashing first makes every guess cost the same 32 bytes against 32 bytes.
There is one more of these, and it is the sort of thing you only notice when you assume the caller is patient and automated. An unconfigured door could simply refuse immediately, without comparing anything. Then refusing would be measurably faster than refusing a configured door, and the time to be turned away would quietly say which doors exist and which are still empty. So an empty door is compared against a random value generated once per process. It costs a microsecond and removes a whole class of question.
What the machine operator is bad at is noticing
Everything above makes the system quieter to a stranger. The same choices make it quieter to us, and that has to be paid for deliberately, in two places.
The distinction the caller is denied goes into the log instead: one line, at error level, naming the environment variable that is missing — once per process rather than once per request, or the signal arrives inside its own flood. And the migrations run before the process starts, with a failure stopping the release outright, because the alternative is a service that comes up and discovers the missing column at request time, on a schema nobody knows the shape of, with no operator watching the deploy that caused it.
The tests were green, and one of them was lying
Fifteen deliberate breakages, one at a time, to watch the suite go red and read what it said — and a sixteenth run with nothing changed at all, which has to come back green or the other fifteen prove only that the suite can fail. Three came back wrong, at three separate moments, and every one of the three was wrong in the tests rather than in the code.
| What was broken | What the suite said |
|---|---|
| an unconfigured door lets everyone through | red |
| the refusal says which key was wrong | red |
| the refusal becomes cacheable | red |
| both doors share one key | red |
| the comparison only looks at the first eight characters | red |
| the operator is told on every request instead of once | red |
| the log carries the key that was tried | red |
| an administrator page appears | red |
| the endpoint list is published again | red |
| two articles may claim one address | red |
| the migration has no way back | red |
| the connection string loses the encryption it was given | red |
| the migration path drops that setting instead of renaming it | red |
| a database pool that failed once is never retried | red |
| the readiness check hands the driver's error to the caller | red |
| nothing is changed | green |
The first two shared a cause. The migration tests all ran against one database, and the migration tool records what it has already applied inside that database — so the second test found the schema already up to date and never ran the migration it existed to exercise. Break the constraint that stops two articles claiming one address and the suite stayed green; change nothing at all and the suite went red. Those are the two answers a test harness must never give, and it gave both in the same run.
The third arrived later and was quieter. A check that the readiness endpoint never repeats the database driver's own error to a stranger looked well covered, and the mutation that made it repeat that error survived anyway: every existing test pointed at an address with nothing listening, so the connection failed before any query ran, and the line that would have leaked was never reached. The guard was real, the coverage was imaginary, and nothing but breaking it on purpose would have said so.
And one sentence I had written down was simply false
A review of the change caught something else worth keeping. The connection string arrives with a setting on it that says how strongly the link to the database must be encrypted, and my code was deleting that setting on the way in — with a comment beside it explaining that the database driver rejects it.
The driver does not reject it. I checked, against the real driver rather than against my own comment, and it reads the setting fine; what rejects it is a different library in the migration path, which spells the same thing under another name. So the deletion was fixing a problem the application did not have, by quietly weakening the encryption of every connection it makes. The setting is now renamed for the one consumer that needs it renamed, and removed from nothing.
The general shape is the part worth carrying: a requirement that vanishes on the way to where it applies is the one kind of failure that looks exactly like success. Nothing errors. Nothing logs. The connection is made, and it is made worse.
What is not proven
None of this has run on a server. The service is built, the schema migrates, the lock holds against twelve attacks and the keys exist — but the process that will actually answer on the internet does not exist yet, because creating it is somebody else's hand on somebody else's account, and that is a boundary worth keeping even when it is inconvenient.

