The Maintenance Window Is the Real Work

A maintenance window with hard gates tells you whether you actually trust your system. Not in theory, in practice. I ran one on Shipstry that took 24 minutes and taught me more about building durable software than three months of feature work.

Feature work is the easy thing to talk about. Screenshots, launches, changelog entries. There’s always something visible to point at. I’d been doing that for months: comments, badges, discovery pages, flexible pricing.

The less visible work is what happens when the data model under those features stops being a clean fit. Feature work lets you ship something and call it done; maintenance work is when you have to defend what you already shipped. That was the work that taught me what I actually trust.

Why I stopped trusting my own migration history

The upgrade had two jobs: split a unified comment table into product and blog comments, and replace a single order model with a cleaner payment domain, payment_order, purchase, product_submission_purchase.

Neither was technically hard. Both required answering a question I couldn’t shortcut: does the data in production match what my assumptions say it should?

The answer came back no, before I’d even started the main cutover. I checked for old pricing-tier values that earlier migrations should have cleaned up. They were still sitting in production. It meant I couldn’t trust migration history in the abstract — only the database I was actually about to operate on — so I reran the normalization first. Production has no interest in your assumptions, and this work gets easier once you stop arguing with it.

Before touching anything, I exported production D1 and recorded baselines for every table I was about to modify. Legacy comment count. Legacy order count. How many backlinks purchases referenced the old model. The exact paid total for every product that had gone through an earlier pricing tier.

That last one turned out to be the key. When I backfilled historical data into the new payment model, some old order rows couldn’t tell me the precise boundary between an original submission and a later upgrade. The schema hadn’t stored that distinction.

I had a choice: invent the missing data to make the new schema look complete, or keep the backfill conservative and live with the gap.

I chose the second option. Cleanly modeled fiction is still fiction. The database is not the place to make things look neat at the expense of accuracy.

The maintenance switch is the real work

The single most important thing I did before the upgrade was not a migration script. It was a centralized MAINTENANCE_WRITE_FREEZE guard wired through every endpoint that mutates D1: checkout creation, Stripe webhooks, comments, likes, votes, profile updates, admin actions, draft flows, notification endpoints.

If the migration scripts failed, I still had control. That’s the difference between a maintenance window and an incident.

The one surprise

The first run of the payment backfill failed. Not because the logic was wrong or the data was bad. D1 remote execution refused the explicit SQL transaction wrapper in the file.

If I’d been running the window without a write freeze, that failure would have been a problem. With the freeze in place, it was just friction: remove the explicit BEGIN TRANSACTION / COMMIT, rerun, verify the gate counts still matched, move on.

The failure didn’t matter because the system around it was designed to absorb failures. That was the whole point of the window: not to execute the scripts perfectly, but to have a setup where surprises stay small.

(I should probably have tested the transaction syntax against D1 remote before the window. I didn’t. Next time.)

What changed

The schema looks different now. The payment model is cleaner. Comments know whether they belong to a product or a blog post. Historical backlinks access still works. Old paid amounts still count toward upgrades.

But the real change is less visible. The exercise of planning the window, building the guard, recording baselines, and validating each gate forced me to understand my own system at a level that feature work never does. You don’t really know a codebase until you’ve had to prove, under a write freeze, that every assumption you made about the data is still true.

I wouldn’t trade those 24 minutes for another three months of features. Both matter, but only the maintenance window told me whether I actually trust what I built. Shipstry now has a foundation I trust to keep building on.