One boolean answering two questions

Written August 2026

From building a multi-tenant business platform single-handed — one of four notes on what the work actually taught.

The platform had a column on the workspace table called is_demo. It had picked up two meanings, and for years nothing revealed that they were two.

The first meaning was the data here is disposable — checked before an automated run was allowed to write anything, so that a test could never create and delete records inside somebody's real workspace.

The second was do not touch the outside world — enforced over every function that sends an email, a text message, a webhook or a legally binding electronic invoice, and the reason a demo workspace withholds its outbound integrations.

For a workspace shown to prospects, both are true at once, so the overload never showed. It showed the day I needed a bench: a workspace inside the production database that end-to-end runs could be driven against, because a run on a laptop proves the code and not the deployment.

The bench needs opposite answers

It exists precisely to be written to and thrown away — the first meaning, true. And the set of behaviours it could not prove was exactly the set the second meaning blocks: outbound mail, messaging, webhook dispatch, invoice transmission. Everything I most needed evidence for.

The obvious fix would have unblocked ten surfaces and, in the same statement, broken every test that had already earned its label.

Clearing the flag was one UPDATE. It would also have made the workspace resolver refuse the bench, because that resolver reads the same column to decide what may be written to — so every process test that had earned a maturity label would have stopped being re-runnable. One statement, one surface unblocked, all the evidence invalidated.

So it is a split, not an update

The migration separated the column into is_disposable and is_demo, and then set them independently: the prospect-facing workspace keeps both, the real one has neither, the bench is disposable and not a demo. Two questions, two columns, and each guard now reads the one it actually meant.

The general shape is worth more than the instance. A boolean that has quietly accumulated a second meaning is invisible while every case agrees, and the case that disagrees is usually the one you built on purpose to test something. When a flag starts gating behaviour that has nothing to do with its name, that is not a naming problem to be fixed with a rename. It is two columns wearing one hat.

One postscript, because it cost more than the migration did. A branch that had forked before this landed rewrote the surrounding documentation wholesale and merged cleanly, so for ninety minutes the written record described the old, safer-sounding arrangement while the schema had already moved. A merge that resolves without conflict is not a merge that kept what the other branch said — and the half that survived was the false one.

What this is from

These come from one project: a multi-tenant business platform for professional firms, where the customers, the projects, the hours, the quotes and the invoices are joined end to end instead of sold as five tools that happen to share a login. Around it sit the public sites, the documentation and the developer tooling it is built with — seven deployed applications and one Postgres database, in a single repository.

It is built and run by one person, outside the hours of a full-time job. That is context rather than a boast, and it is the reason the notes read the way they do: alone, you cannot out-work a mistake, so what matters is leverage — what to automate, what to refuse to claim, and which checks earn back more than the time they cost.

The code is private and none of it is given away here. What these describe is method: how something was measured, what the measurement contradicted, and what changed as a result. A method is worth more read than hoarded — and it is the part nobody can copy without first understanding it.