Skip to main content
BlueForge
EngineeringJune 30, 20264 min read

Taking Four Products Off Supabase

Kristian Mandrup

Founder & CEO

We had standardised on Supabase. In June we took four products back off it.

This is not a post about Supabase being bad. It is a post about what a managed data layer actually costs once you have four products, and about the one migration that did not work.

The one that went smoothly

SocialSphere was the template. It went in a single day as a 25-task sweep, plan committed to the repository alongside the code: Supabase Realtime was replaced with a WebSocket relay over PostgreSQL LISTEN/NOTIFY, hot storage moved to object storage, and both NextAuth and Supabase Auth came out in favour of forge-auth with Drizzle and bcrypt. Twenty-five tasks, one day, one design document that says what each task replaces.

The reason it went well is boring and worth stating: realtime was the hard part, and we did not try to keep it. We replaced the mechanism.

The one that took three days

forge-control is the biggest of our products by commit count and it needed its whole data layer replaced. Three commits in three days: the migration off Supabase to direct Postgres, then the cleanup, then the Drizzle upgrade and the last of the Supabase-era code removed. Production monitoring and the storage layer followed, Drizzle-first.

Two things made it survivable. The first was that we wrote down what was broken: one commit exists purely to document a migration system that had been gutted along the way, because a migration system that half-works is more dangerous than one that plainly does not. The second was sequencing — the work landed as a single merged branch rather than as a stream of small changes to a live data layer.

The one that was already done

PropertyFlow had the same shape a few weeks earlier, in the other direction: eighteen tables with row-level security created in a single commit, plus a data-copy script and a phase that removed Firebase entirely.

One design decision there is worth stealing. Firestore IDs are strings, and the conventional advice is to use UUIDs in Postgres. We made the ID columns TEXT instead, so the copy could preserve every existing identifier exactly. A schema that is slightly less idiomatic is much cheaper than a data migration that has to rewrite every foreign key while users are holding the old ones.

The one that hit a wall

RoomCraft did the schema and the API cleanly: seventeen schemas of generated Drizzle migrations, a scaffolded API, fifteen routes wired up. Then the data migration step failed with a message that says everything:

phase 6.5 — skip data migration, source Supabase unreachable

Forty-three commits over two days, and the last thing standing between the old system and the new one was the data itself, in a place we could not read. The code moved. The rows did not.

What we took from it

Schema and code are reproducible; a repository holds them and a deploy applies them. Data is not. It exists in exactly one place, and that place is either reachable on the day you need it or the migration becomes a decision about what to throw away.

The lesson we wrote down is the one we should have known: for any data migration, pull a full copy to storage you control before you announce the cutover — not because you plan to need it, but because the day you need it is the day you cannot get it.

What's Next

Two of these systems are now ours to operate, which means two new classes of incident are ours to have. Most of what follows in this blog is about learning that the hard way.

Stay in the Loop

Get early access to our products and be the first to know when we launch.

Join the Waiting List