Don't Let Users Write Mid-Migration: Maintenance Mode and Read-Only Windows for Vibe Projects
Maintenance mode is not a sign on a page; it is a mechanism that keeps user data intact while you migrate. This guide covers three levels, the four-part backend switch, easily missed chains, and three-phase communication.


__MEDIA_1__
One sentence to get the principle straight: maintenance mode is not a "we are under maintenance" sign. It is a mechanism that guarantees that while you are changing the database and billing logic, data users write does not get lost, corrupted, or left half-written. Anyone can hang up a sign; the hard part is making sure that once the sign is up, not a single write request, scheduled job, queue, or webhook slips through. This guide covers how to build that mechanism completely.
The example product "Spruce Ledger" and its founder "Lu Yao" in this article are fictional, used only to explain the flows. They do not correspond to any real product.
Boundaries: this is not a status page or an incident runbook
This site already has a status-page guide and an incident runbook, which cover "something broke — how do we communicate and recover?" This guide covers the opposite case: the work is scheduled by you — a database migration, a billing-system switch, a major structural change — and you know in advance you will need to pause for a while. Two neighbours are also worth distinguishing: canary releases ("let a fraction of users try the new version first") and feature flags ("toggle one feature on or off"). Maintenance mode is different: it pauses all writes site-wide or for a whole category of writes. Do not substitute one for the other.
Whether maintenance mode is needed comes down to a single test: while the change is in progress, if users keep writing data, will it produce dirty data that cannot be fixed automatically afterwards? Renaming database fields, restructuring billing tables, rebuilding the permission model — the answer is yes in all three cases, so maintenance mode is required. Pure frontend redesigns, adding fields without touching old ones, or adding a new table usually need no write freeze. Do not hang the sign for ceremony — every unnecessary outage spends down user trust.
Three levels: do not jump straight to a full shutdown
Maintenance mode is not just "on" and "off." Solo teams most often need the middle levels; decide them in advance so nobody has to improvise on change day:
- Full maintenance: every page shows the maintenance page and every write is rejected. Reserve this for database migrations and infrastructure-level switches where any movement could corrupt everything. Keep the window as short as possible — if it can be 20 minutes, do not stretch it to 2 hours.
- Read-only mode: users can view and export but cannot write. This is the most underrated level: most migrations only need "no new data coming in," while old data can be viewed freely. Read-only minimises user anger, because a need like "I want to check an old bill" is not blocked.
- Partial shutdown: only the affected features close — for example, "create new bill" is off, but viewing, exporting, and settings all work. This fits billing switches and single-module rewrites. It costs the most to implement, because every entry point needs its own check, but it gives the best user experience.
Spruce Ledger (fictional) went straight to full maintenance for its first billing-table migration, kicking users out mid-reconciliation, and its support inbox exploded overnight. The second time it used read-only mode: viewable but not writable, announced in-app a week ahead, and that night's complaints were zero. Choosing the right level makes exactly that much difference.
Implementation: one switch that the entire chain respects
The most common maintenance-mode failure is "the frontend hung the sign, but the backend kept accepting orders." Users see the maintenance page, then write data anyway through an old app version, a browser plugin, or direct API calls — and after the migration, reconciliation is full of ghost data. So the switch must live on the server, and every write path must respect the same switch.
The minimum viable implementation has four parts: first, a server-readable config switch (an environment variable or a config table — never something that lives only in frontend code), checked at the entry of every write endpoint; second, rejected writes return HTTP 503 with a Retry-After header, telling callers "come back after this long," rather than a 200 with a "system under maintenance" message — the former lets clients and retry logic handle it correctly, the latter gets treated as success; third, the static maintenance page must be deployed separately from the main app (a plain HTML page on object storage is enough), so users see the maintenance page even while the main app is restarting, instead of a connection timeout; fourth, admins need a bypass channel (for example, an internal account with a specific header), or you cannot even get in to verify the migration result.
Beyond the switch, four chains are easy to miss and must each be shut down or drained before the change: scheduled jobs (reconciliation, charging, or mailing jobs that run mid-migration may write data in the old structure); message queues (queued tasks will keep executing after migration, so confirm they read the new structure before they run); third-party webhooks (payment-success callbacks arriving during maintenance must not be dropped — accept them, store them, and replay after recovery, instead of returning failure and letting the sender give up); and client-side cached "offline writes" (offline submissions saved in the app will flood in after recovery — watch the write-rate curve closely during the first hour back).
Communication rhythm: announce early, update during, and announce recovery too
Get the technology right but the communication wrong, and users will still think you are unreliable. Maintenance communication has three phases, and missing any one causes problems:
Announce at least a week ahead, in a place users actually see daily: an in-app banner, a post-login modal, or email — any one will do, but not only a blog nobody reads. The notice must state four things: start and end times (with timezone), scope (is the whole site unusable, or view-only?), what users need to do beforehand (for example, "please finish this week's reconciliation before Friday 22:00"), and what will change after recovery. Do not write empty phrases like "system upgrade, stay tuned." Users want to know "will I lose data," not your upgrade slogan.
The in-progress maintenance page must also state four things: the current status, the expected recovery time (a concrete time, not "as soon as possible"), what users need to do after recovery (usually "nothing"), and an emergency contact (an email address is enough). If the expected time slips, update the time on the page too — users can accept a longer maintenance window, but not a page whose time never moves, as if nobody is watching.
After recovery, send one short announcement: maintenance is complete, how long it lasted, and anything users should notice. If the migration found and fixed data problems for specific users, notify those users individually rather than glossing over it in the announcement. Trust is built up exactly this way, one time at a time.
Five traps in read-only mode
Read-only mode sounds simple, but five traps specialise in catching first-timers: first, search and export features may internally write data (recording search history, generating export files) — these "look like reads but are actually writes" need to be explicitly allowed or temporarily disabled; second, "write buttons" must not merely be greyed out — a greyed button does not stop direct API calls, so backend endpoints must reject writes too; third, think through login and registration: can new users register during read-only? Usually registration should be allowed but read-only afterwards, or a new user's first impression is a closed door; fourth, caching: CDN and app caches may still hold old pages during maintenance, so purge key pages after recovery, or users will keep seeing the maintenance page; fifth, after recovery, do not reopen everything at once — open for 10 minutes, watch write volume, error rate, and queue backlog, and only announce recovery complete once the curves look normal.
A checklist you can copy directly
One week before the change: choose the level (full / read-only / partial), write the notice copy, send the in-app banner and email, prepare the static maintenance page, confirm the admin bypass works, and list every scheduled job and queue with a pause-and-drain plan. On change day: send one more reminder 30 minutes ahead, flip the switch on time, verify writes genuinely return 503, verify the maintenance page is reachable, pause scheduled jobs, and start the migration. After migration: verify reads and writes internally, purge key caches, open for 10 minutes of observation, send the recovery announcement once all looks normal, resume scheduled jobs and replay webhooks, and review write volume and error logs the next day.
One last reminder: the real cost of maintenance mode was never the sign. It is whether, before you start, you asked completely "who else could still write in here?" Ask it completely, and a 20-minute migration stays 20 minutes; miss one channel, and you will spend a weekend fixing dirty data afterwards — while users remember only that one time.
Where to store the switch: three options compared
A maintenance switch sounds like a single boolean, but where it lives directly shapes change-day operations. Three common options trade off as follows: environment variables are simplest — one change requires an app restart, which fits windows where a restart is already part of the plan, but changes are slow and a mistaken change needs a restart to roll back; a config table is most flexible — one row change in the admin takes effect immediately and records who flipped it when, which fits "open for 10 minutes of observation, flippable back at any time" — but if the config table itself is being migrated, you get a circular dependency of "using the migrating database to control the migration," so the switch table must live in a database or config service untouched by the migration; a feature-flag service is lowest-effort but adds another external service and another bill, usually overkill for a solo team that maintains only two or three times a year.
Spruce Ledger's (fictional) approach is worth copying: the switch lives in a standalone config table with just four columns — key, value, updated-by, updated-at; the maintenance key is named separately and never migrates with any business table; the admin page puts a double-confirmation dialog on this switch to prevent accidental clicks. On change day, Lu Yao (fictional) alone can flip it from the admin page — no code change, no restart, no help needed. That is what a solo team's maintenance switch should feel like.
A minute-by-minute timetable for migration day
Write change day as a minute-by-minute timetable in the team docs (even if the team is just you); it prevents half of all change-day chaos. Here is a read-only migration starting at 22:00 and expected to take 40 minutes — adjust the times to your case, but the structure can be copied directly: 21:30, send the final reminder (in-app banner plus email) telling users "from 22:00 you can view but not record"; 21:55, confirm queues are drained, scheduled jobs are paused, and the webhook receiver has switched to "store now, process later"; 22:00, flip the read-only switch, immediately verify with a normal account that "viewing works, writes return 503," and verify with an admin account that the bypass channel works; 22:05, start the migration, updating the maintenance page every 10 minutes during it ("migrating the billing table, expected recovery 22:40"); 22:35, migration done — verify reads and writes internally, then purge key caches; 22:40, open for 10 minutes and watch write volume and error rate; 22:50, once normal, send the recovery announcement, resume scheduled jobs, and start replaying webhooks; next morning, review error logs and write-volume curves.
The two items most often left out of the timetable are "verify writes are genuinely rejected" and "verify the admin bypass works." Miss the first and the sign is hung but the door is unlocked; miss the second and you lock yourself out and cannot verify the migration result. Both verifications take under 2 minutes combined, yet they block the two most embarrassing failure modes.
Post-recovery review: a record the next run can follow directly
A post-maintenance review does not need to be long; six items suffice: planned versus actual window length, how many users were actually affected (read-only-period traffic and rejected write count), whether any dirty data appeared, how it was found and fixed, whether any users complained and about which part, and what to change for the next similar migration. Spruce Ledger's (fictional) second migration review was half a page, but for its third change — a billing switch — Lu Yao (fictional) executed the "webhook replay order" from that review line by line, with zero incidents. A review's value is not in looking polished; it is in being directly followable next time.
The review should also record "was communication adequate" separately: advance-notice open rate, maintenance-page traffic, and whether users kept asking "why can't I use it yet" after the recovery announcement. If users still report seeing the maintenance page an hour after the announcement, the cache was almost certainly not purged properly, and next time "purge caches" should be split out as its own recovery step rather than remembered afterwards.
FAQ: the five questions you will definitely be asked before launch
"Can we skip maintenance mode and migrate online?" — Often yes: adding fields only, adding tables only, and dual-write transitions are all mature online-migration techniques. This guide's position is not "hang the sign for every change," but "when you judge online migration would create dirty data that cannot be fixed automatically, do not tough it out." Dual-write transitions have their own cost; price it before choosing.
"What happens to a form a user is halfway through filling in?" — The form page should warn before the switch flips: "read-only starts at 22:00, please submit beforehand." A user who submits after the switch gets a 503 with clear copy: "the system is under maintenance; your content was not saved; please resubmit after recovery." A kinder touch is prompting users with unsaved forms 10 minutes before the switch to copy their content out locally — but do not promise "auto-submit after recovery," which turns a simple problem complicated.
"What about app users on old versions that never upgrade?" — When an old app version does not recognise the maintenance switch and keeps calling write endpoints, the backend 503 is the last line of defence. That is why the switch must live on the backend, not only in frontend checks. Old-version users may see a generic error, so the error copy should say "the system is under maintenance, please try again later," not a bare error code.
"When is the best maintenance window?" — Look at your user activity curve, not at a guessed "middle of the night." Spruce Ledger's (fictional) users are least active after 22:00, so its window is 22:00; if your users span many timezones there is no hour when nobody is active, so pick the least-bad slot, keep the window as short as possible, and use read-only instead of a full shutdown.
"What if we need maintenance many times a year?" — More than four times a year means the change process itself needs improvement: split big migrations into smaller ones, do online what can be done online, and use dual-write transitions instead of downtime. Maintenance mode is a seatbelt, not a routine operation; a seatbelt used four times a year is protection, but one worn every day means something is wrong with the process.
One more note: if this is your first maintenance mode, do not stake the first run on your most important migration. Pick a low-impact change and rehearse the full flow once — announce, go read-only, verify rejection, recover, announce, review. It takes half a day at most, but on the night of a genuinely critical migration, you will know what the next step is at every point.
Comments (0)
Related articles

Magic Link is the best-value passwordless option for a solo team, but login security then depends on mailbox security and email deliverability. This guide covers seven link-security rules, delivery testing, cross-device handling, and fallbacks.

A demo mode lets anyone try your product with one click — data that looks real, flows that run end to end, and zero writes to real data. This guide covers seed data, isolation, reset, and exit paths.

Solo projects rarely die because they cannot be built. They die because, once put down, they cannot be picked up again. This guide covers the five-part continuity handbook, how to write for a forgetful future self, resuming AI-assisted work, and the day-one restart order.