extrua
← All posts
Build notes·20 August 2026·6 min read

We don't run a test environment. That isn't a corner we cut — it's why a bug gets fixed the same day.

You've had this happen. Some tool you rely on updates itself overnight, and on Wednesday morning a thing that worked on Tuesday doesn't. A report comes out empty, or a button's moved, or the job you do forty times a day now takes an extra click. And somewhere in that vendor's office, that change was tested. It passed. It shipped anyway, and it still broke your Wednesday.

There's an old joke among people who run servers: everyone has a test environment, some are just lucky enough to also have a production one. It's aimed at cowboys. I want to make the less comfortable argument — that the joke is true in the other direction too. A test environment is a copy of your software running in a world somebody invented, and the bugs that actually reach customers are, almost by definition, the ones that invented world didn't contain.

We don't run one. This is why, and what sits in its place — because the honest answer isn't bravery, and if it were bravery you shouldn't buy software from us.

What a test environment actually is

Strip the jargon off and it's simple: a second copy of the app, pointed at a second database. Before a change goes to customers it goes there first, somebody clicks around, and if nothing falls over the change goes out. Sensible. Almost every software company does it, and the ritual is genuinely reassuring.

The crux is the second database, because someone has to decide what's in it. Someone sits down and creates a few customers, a few jobs, a few invoices. And what they create is tidy, because a person imagining a customer imagines a reasonable one.

Real data is not tidy. Real data is a customer whose business name has an apostrophe in it. A job rescheduled four times and then half-cancelled. A recurring visit whose series was ended in March and restarted in April, so it now has two parents. An invoice part-paid by two payments in different months, one of which arrived before the invoice was raised. A phone number typed into the address field by someone in a hurry on a Friday. Nobody seeds that, because to seed it you'd have to have thought of it — and if you'd thought of it, you'd have written the code to handle it in the first place.

A test environment can only contain the situations somebody thought of in advance. Those are precisely the situations the code already handles. The bug you actually ship is the one nobody imagined — which makes it, structurally, the one the rehearsal cannot contain.

The three ways the rehearsal lies to you

It's worth being specific about the failure, because "staging is imperfect" is the sort of thing everyone nods along to and nobody acts on.

  • The data is fiction, and it's optimistic fiction. Every seeded record was written by someone who understood the system, so it obeys rules real records were never told about. You are testing against a world that agrees with you.
  • The scale is wrong, and it's wrong in the flattering direction. A query that reads a whole table is instant on two hundred rows and a timeout on two hundred thousand. A test environment is the one place on earth where a slow query looks fast, and it will tell you so right up until the morning it doesn't.
  • It drifts. The schema is a few weeks behind, one setting is on there and off in production, a key points somewhere else. So a green tick means the change works against last month's shape of the world — which is a real fact, just not the one anyone reads it as.

There's a fourth problem that's more about people than software. A rehearsal that passed becomes a reason not to look afterwards. "It worked on staging" is one of the most expensive sentences in this industry, and it's expensive precisely because it's usually true.

What replaces it

Deleting the rehearsal is only defensible if something harder takes its place, and the honest version of what we do isn't clever — it's just checks that run on every single change, automatically, that nobody can wave through because it's late and the fix is obviously fine. That last clause is doing most of the work. A check you can skip when you're in a hurry is a check that is absent exactly when you need it.

  • Types and linting across the whole codebase, run by the machine rather than on whoever's laptop happened to be open. A surprising share of real breakage is something being null that the author was certain could never be null.
  • Isolation checks. In software several businesses share, the worst class of bug is a query that forgets which business it's for. So a check fails the build when something that should be scoped to one customer isn't — the machine reading every path, rather than a person remembering to.
  • Database changes that only ever go forwards, numbered in order, each one applied exactly once and never edited after the fact. Most horror stories in this area start with someone quietly changing a migration that had already run somewhere.
  • Checks that exist because of one specific bad day. Each is narrow and slightly odd out of context — which is the point. They're fossils of real mistakes, and they're the ones that have earned their place.

None of that is exotic. What makes it work is that it's mandatory and automatic, which are two words the rehearsal ritual never quite manages.

Then ask the data, not the code

Everything above asks a question about the code: is this written correctly? The part I'd actually recommend to anyone building anything is the inversion — a couple of dozen checks that ask a question about the world instead. Not "could this go wrong," but "did it?"

They're plain questions asked directly of the live data, on a schedule. Is there a completed job with no invoice attached to it? A recurring series that quietly stopped producing visits? A record that belongs to two businesses at once? A payment allocated to an invoice that no longer exists?

The property that makes this worth more than any amount of testing: a result here isn't a hypothesis. It isn't a warning that something might one day go wrong under unusual conditions. It's a row, with a date on it. It already went wrong, on a real Tuesday, to a real record — and nobody rang up about it, because it didn't look like a failure. It looked like nothing at all.

Testing asks whether the code could go wrong. Querying the live data asks whether it already has. Only one of those questions has ever been answered by a row with a date on it.

That's the category of problem that quietly costs an operator money — the job that never got invoiced doesn't announce itself, it just isn't there. You can't test your way to finding it, because there's nothing wrong with the code today. Something was wrong with it in April.

Every incident ends with a guard, not a fix

The rule we actually run on is this one, and it's the only part of this post I'd defend as genuinely non-obvious. When something breaks — a bug, a bad assumption, a claim we made that turned out to be wrong — fixing it is the first half of the job and the less important half. The second half is to name the class of mistake, build an automatic check that fails if anything in that class ever happens again, and then deliberately break it to confirm it actually fails. A check nobody has tried to break is not a check. It's a comment with more steps.

The bar we hold ourselves to, in the words we wrote it down in: a guard that only runs when someone remembers to run it is a memory, not a guard. Memory is the thing that failed in the first place.

The compounding is the whole benefit. The list only ever grows, it costs nothing to keep running, and it means a given mistake gets exactly one chance at us. That's a much weaker claim than "we don't ship bugs" — we do — but it's one we can actually stand behind, and you can hear the difference between the two.

Where we'd be wrong about this

This isn't universal advice and I'd be selling you something if I pretended it were. If a bad hour can't be undone — money moving irreversibly, anything medical, anything where an aircraft is involved — rehearse it, then rehearse it again. The calculation changes completely when the worst case isn't recoverable.

And if a team has no automatic checks at all, getting rid of the test environment isn't a philosophy, it's just deploying without looking. The rehearsal is a poor safety net, but a poor safety net comfortably beats none. What we're arguing against is treating it as the safety net — the version where staging is green, everyone relaxes, and nothing downstream ever asks the live data a question.

The straight version of our position: we break things, same as everyone. What we've built is a system where the same thing doesn't break twice, and where finding out is our job rather than yours.

Why any of this is your problem

Because it decides the only number you actually feel: how long between you reporting something and it being fixed. When work goes out continuously and the checks are automatic, there's no queue between the two. Something reported at ten can be fixed by lunch, because there's no release window to wait for and no rehearsal to schedule. The checks aren't the thing that slows that down — they're the only reason moving that fast isn't a gamble.

You don't need to audit anyone's pipeline to get at this. Four questions do it, and you're listening for whether the answer is specific or vague.

  • How do you find out something's broken — do you hear it from a customer first? Every vendor sometimes does. The interesting bit is whether that's the only way they ever find out.
  • What happens after a bug is fixed, other than the fix? If the answer is "we fixed it," that class of bug is free to come back, and eventually it will.
  • How long between me reporting something small and it being fixed? You're not after a promise, you're testing whether a queue exists between finding and fixing.
  • Do you ever check your live data for problems, or only wait for reports? Almost nobody does this. A vendor who does will tell you about it at length, because they're pleased with themselves — which is exactly the reaction you want.

Where we land on it

We're a small studio that also runs a real services business, so the software gets used all day by people who will tell us bluntly when it's wrong. That's a genuine advantage and it's also the reason this approach suits us: a fast feedback loop is worth more than a careful rehearsal when the loop is honest. In Dispatch, the same instinct shows up in the product — the AI reads your enquiries and drafts the replies off your own price list, and the send button stays with a human, because a draft a person glances at is a check that runs every time.

The wider point isn't really about test environments. It's about what a safety measure is for. A rehearsal makes the person shipping the change feel better, and there's nothing wrong with feeling better — but the measure that protects you is the one that runs whether anyone's paying attention or not, on the real data, on the day nobody's watching. Those two things get confused constantly, and the ritual is nearly always the one that survives, because it's the one you can see.

Software for service businesses — built by an operator.

Job management, books, and AI agents that actually know your business.