AffinEQ

Product

Platform overview Integrations Verified, not claimed What isn't built yet

Solutions

Mobile-first startups Agencies building for SMEs Fintechs & regulated teams What people build

Migrate

How a move works From Supabase From Appwrite From Firebase From AWS Amplify From any Postgres Pricing

Developers

Documentation Next.js quickstart JavaScript quickstart Changelog Support Contribute Platform status

Company

Why we exist Customers Blog Partners Contact Sign in Start building
Blog/Migration

Moving a live app off Supabase, as a copy

The best test of a platform is an app that was written by someone who had never heard of it. Ours was a live store, running on a self-hosted Supabase, with real customers, real orders, real payments, and a lot of server code. We wanted to move it onto AffinEQ. We were also determined not to harm it on the way.

Rule one: never point the importer at the live database

Reading a production database is load on a production database, at the worst possible time, from a tool we had just written. So the importer never connects to the source at all. It reads a plain pg_dump file. The dump is taken on the server and stays there, readable only by root. It never goes to a laptop, because it holds a client’s customers.

Reading what is in there

A Supabase database is more than your tables. It has Supabase’s own schemas, extensions, triggers and roles. The importer reads the dump and sorts every object into keep (your tables, your policies), skip (Supabase’s own machinery we replace), route (things that belong somewhere else on our side, such as a cron job becoming a Schedule) and block (something we can’t carry across yet, which stops the import and tells you why). It does a dry run first, so you read the plan before anything moves.

One transaction, and a count for every table

The load runs in a single transaction into an empty project: it all lands, or none of it does. After the load it counts the rows in every table and compares them with the dump’s. A migration that “mostly worked” is the worst outcome, because you find the gap in production, so any difference is a failure. Users are loaded through a staging step, since their passwords need care: the imported hashes still work, and they are upgraded to our own hashing the first time someone signs in.

Then the parts that aren’t tables

  • Realtime becomes Live, switched on for the tables the old system watched.
  • Storage buckets get their records created, so the structure is there.
  • Cron jobs become Schedules, and every one arrives switched off. Some of them touch live data, and we did not want a second system doing the old one’s nightly work while both were running. Turning them on is a deliberate step at cutover.
  • The database webhook becomes a change hook on a Worker.
  • The edge functions run on Workers, unchanged, through small compatibility shims: the environment variables, the auth admin API and the standard server entry point answer the way the code expects. We chose that over rewriting a client’s working code, because the code is theirs, it works, and it is not what we are replacing.

The secrets those functions use are the sensitive part. They move on the server only, through root-only files and pipes, never in a command line, a log or a printed line.

Proving the copy is the copy

Running both systems in parallel for weeks is how you earn the right to switch, but only if you can tell whether they still agree. So we built a comparison tool. It takes a fresh dump of the original and the migrated project and reports, per table, per user and for the parts that were routed, whether they match. It reads only counts and names, never rows, and exits with an error if anything has drifted, so it can run unattended. When we ran it on the imported store, every table and every user count was identical.

What we haven’t done

We have not cut over, and that is on purpose. The copy runs beside the original. Before the store’s writes go anywhere new, there has to be a backup of the new place, restored and proven, and the off-host copy of it has to exist. Parallel running protects the old data; it does nothing for the new. Cutting over first and taking backups later would leave a window where one lost disk loses real sales.

The importer is run by us, alongside you. It isn’t a button yet, and importers for Appwrite and Firebase come after this one has been proven on more real apps.

Move a copy. Compare it until the comparison is boring. Then, and only then, switch.

If you are on Supabase and curious how your app would move, tell us what you’re running. We’ll tell you plainly how it would go, including what wouldn’t work.

Already know the Supabase client? The change in code is the import and the key. See Coming from Supabase.
← All posts

Type to search. Nothing you type leaves this page.