the migration kit
Fifteen apps moved from consumer hosting to the agency's internal platform — and the process itself became the product. The first took four days; the last three took one.
team one internal documentation and fleet; platform specifics generalized, app names mostly described rather than named.
The fleet was prototyped fast on consumer hosting, which is the right place to prototype and the wrong place to run client-data tools. By spring 2026 the agency had its own Kubernetes-on-AWS platform with managed Postgres, secrets, and a CLI — and a growing list of apps that needed to move with full database continuity, no dead links, and no lost weekends. The first migration, a program-planning tool in mid-May, was done the hard way: four days, every surprise discovered live.
After the second app it was obvious this wasn't a series of migrations — it was one migration, run repeatedly, by whoever (or whatever) was available. So the deliverable changed from “app N on the platform” to “a runbook that makes app N+1 boring.” Everything learned went into a stack-agnostic guide with numbered lessons, a per-app checklist template, and a platform reference grounded in the platform's actual CLI behavior and source code rather than its docs — because its docs were wrong in at least two places that cost real outages.
Assess what is actually deployed, not whatmain says. One app's old host was deploying a feature branch five commits ahead of main — auth hardening, a model fallback chain, schema changes — and the first migration draft was built from main. The lesson went in as a first step: read the deployment record's branch before you read the code.
Data continuity over elegance. One app had its production database in SQLite on an ephemeral filesystem — 432 rows, one restart from gone. It was pulled over ssh as base64, converted to JSON, and loaded by an idempotent boot import on the new side the same day; row counts are verified in-pod as part of the runbook, every time. Another app's schema had been built with push-and-accept-data-loss on every boot, with no migration history — the kit squashes to a baseline before the platform's migrate-on-deploy runs.
Redirects, not dead links. Every retired user-facing instance gets a redirect: an env-gated 301 on single-service apps, a static landing page where the old host served a SPA, a notice page where the app had integrations. Bookmarks keep working through a one-to-two-week fallback window; then teardown.
Write for the agent that will execute it. The guide is written for AI coding agents: exact failure signatures (“No workload 'app'” after a successful image push is harmless — re-dispatch after resync), operational norms (“rollouts take five to ten minutes to settle; don't spam redeploys”), and explicit statements of what normal looks like, because an agent that can't tell normal from failure will “fix” a healthy rollout into a broken one. Lessons are upstreamed to the shared knowledge repo by PR so every teammate's agents inherit them.
Nineteen numbered so far. A sample: managed services don't provision until you resync, and resync auto-deploys once the database is up (now the norm, three for three). Batch secret writes can silently drop keys while reporting zero errors — always list afterward. The secrets-unset command was broken platform-side — blank the value instead. Startup commands on the old host aren't shell-expanded, so a ${PORT} goes through literally. Adding a root Dockerfile makes the old host build from it and breaks the previous start command. A gitignored migrations folder builds fine locally and breaks the CI image — check what git actually tracks. Push-workflow CI fires before the manifest reconciles. Every workload gets its own subdomain; the bare app slug serves a placeholder page that is not a broken deploy. Never edit a manifest from the platform's web UI — it flattened two apps' manifests in August and took them down for ninety minutes. And the rotation-proof chapter, which earned its own case study.
Fifteen apps re-platformed from consumer hosting with full database continuity and no recorded data loss — program planning, sprint intelligence, tag monitoring, media-buy parsing, editorial QA, a campaign-intelligence API, the AI program hub, release-notes automation, AI-visibility auditing, a guest-sign-in kiosk, the on-call rotation bot (by silent cutover), and in one August week a brand-intelligence app, a social-signal scanner, a content digest, and a case-study intake queue. The last three landed in a single day, with the guide's newest lessons validated by a second migration the same week — one stall reproduced exactly as documented.
Several of the August apps shipped with usage telemetry and the rotation-aware database layer on day one, because both are now checklist items rather than discoveries. Redirects are live for every retired instance that had users. The old hosts are dark and scheduled for teardown after their fallback windows. And the expanded lessons are in review upstream — the boring version of this story is the one where the next person never reads it.
A migration you'll do fifteen times is a product. Write the runbook precisely enough that an agent can execute it — exact failure signatures, operational norms, what 'normal' looks like — and the fifteenth migration is boring. Boring is the goal.
the insight