
Rehearsing the migration you only get to do once
A migration script that passes against 500 hand-exported rows has proved one thing: the field mapping compiles. It has not proved the load finishes, that every foreign key resolves, or that the two hours you budgeted for cutover are actually two hours. You find those out by running the whole job against production row counts on hardware shaped like production, and by running it more than once. Cutover night is the performance. It should be the least interesting run you have done.
A sample run and a full-volume run fail differently
At sample size, every loading strategy looks equivalent. At production size they diverge sharply, and the PostgreSQL docs are blunt about why. Loading a large number of rows with COPY is almost always faster than INSERT, even when you use PREPARE and batch the inserts into one transaction. The fastest path into a fresh table is to create it, bulk load with COPY, then build the indexes, because creating an index over existing data beats updating it row by row during the load. Foreign key constraints behave the same way: Postgres can check them in bulk more cheaply than one row at a time.
None of that matters on 500 rows. All of it decides whether your load window is twenty minutes or six hours. The same doc warns about the other half of the trap: pass --single-transaction to psql or pg_restore and the smallest error rolls the entire restore back, which the manual describes as possibly discarding many hours of processing. Discovering that at 2am, with the old platform already in read-only mode, is a bad way to learn it.
There is a shortcut that looks tempting and is not. You can drop foreign key validation with --disable-triggers, but the Postgres docs point out that this eliminates the validation rather than postponing it, so bad data can land in your tables and stay there. A rehearsal is where you find out whether you needed that shortcut at all.
The rehearsal environment is now a line item, not a project
Building a production-shaped copy used to be the expensive part, which is why so many teams skipped it. On Supabase that argument has thinned out. Each preview branch is its own environment with its own database, Auth, Storage and API credentials, cloned from the main project with the schema and Edge Functions already in place. There is no fixed fee for a branch; you pay for what it burns, and a branch on the default Micro compute size starts at $0.01344 per hour. A rehearsal that runs for six hours costs less than lunch.
Two details decide whether the rehearsal is honest. Branches start with no production data at all, which Supabase did deliberately to keep sensitive rows out of ephemeral environments. To copy real data in, you turn on Include data at branch creation, and that option requires the Point-in-Time Recovery add-on. The branch then matches your project compute size and uses a larger disk, so it costs more than the Micro figure. Worth knowing before the invoice: branching usage is not covered by the Spend Cap, and Compute Credits do not apply to branching compute.
The documented limitations are also the ones a migration will hit. Custom roles created through the dashboard are not captured when a branch is created, and migration conflicts have to be resolved by hand on the preview branch. Both are better found on a throwaway branch than on the real thing.
What you are actually measuring
Three numbers come out of a dry run and all of them are the point. Wall-clock time for the full load, because that is your cutover window plus whatever margin you can defend. A per-table row count reconciliation between source and target, because a load that finishes is not a load that is complete. And the mapping table that ties every legacy identifier to its new primary key, which is the artifact that lets you answer support questions six months later.
That last one is where most no-code exits get painful. Airtable record IDs, Xano auto-increment integers and Webflow item IDs are all referenced from somewhere you have forgotten: a webhook payload, a saved report, an email template, a partner integration. On the Stay World Class migration off Webflow and Xano, we ran a four stage ETL whose whole job was to preserve those legacy IDs while mapping them onto PostgreSQL UUIDs, so nothing downstream pointed at a record that no longer existed. The resulting Next.js, NestJS and Supabase stack moved Lighthouse from 55.91 to 91 out of 100 and cut LCP by 77 percent, but that was the visible half. The invisible half was that no old reference broke.
Run the thing twice at least. The first rehearsal finds the schema and encoding problems. The second one, after you have fixed those, tells you how long the real run takes, which is the number you were after. Rehearse the rollback too, since the only migration plan worth having is one where you can put everything back.
When a dry run is overkill
Plenty of migrations do not need this. If your Webflow site is marketing pages and a blog, there is no relational graph to break and no legacy ID anyone will ever dereference. Copy the content, redirect the old URLs, move on. The same goes for a single flat Airtable table with a few thousand rows and nothing pointing at it from outside.
If you can run both systems in parallel and reconcile at your leisure, the pressure drops too, and the rehearsal matters much less than the reconciliation. And if your data genuinely still fits inside your platform, your costs are flat and nobody is waiting on a feature the platform cannot express, then you are not migrating this quarter and none of this is your problem yet. Webflow and Airtable were the correct call for a real reason, and that reason has not expired for everybody at once. Rehearsals are for the migrations where cutover is a one-way door.
