← Back to blog

Lessons Learned Migrating from Supabase to Convex

The real lesson was not that one backend is magic. It was that a migration gets much less scary when fallbacks, feature flags, and small cutover slices are treated as part of the implementation.

Social card for Lessons Learned Migrating from Supabase to Convex

I spent about five hours in one long session moving one of my apps from Supabase to Convex.

That sentence makes the migration sound cleaner than it felt. It was not one heroic “rip out the database and hope” moment. It was a lot of careful planning, flags, fallback paths, smoke tests, and annoying little edge cases. The useful part is that the boring discipline is exactly what made the migration fast.

The timing was funny too. On June 5, 2026, Jack Friks posted about a related move: shifting storage work away from Supabase and toward Cloudflare. That is not the same migration I made here. Storage is a different problem than making Convex the primary application backend. But it rhymed with the same question I was running into a few days later: when your product gets more real, which parts of the backend should stay bundled, and which parts deserve a more purpose-built home?

I am writing this because I think more builders are going to run into a version of this decision. Supabase is a good product. I have used it, shipped with it, and I still think Postgres is an excellent default for a lot of serious software. But if you are building a bunch of small AI-first products, especially as an indie builder or small team, project limits and dev/prod environment planning start to matter quickly.

The current Supabase pricing page says the free plan has a limit of two active projects. The Supabase billing FAQ also describes each project as its own dedicated instance. That is reasonable from their side. A Supabase project is not just a row in an account table. It is Postgres, Auth, Storage, Functions, Realtime, and the surrounding infrastructure.

But from my side, the shape of the work is different. I want to test multiple product ideas, keep dev and prod separated when the product deserves it, and let AI agents make meaningful backend changes without me babysitting every SQL policy and dashboard setting. Convex’s current pricing page is much friendlier to that experimentation model. I am deliberately saying “deployment” here, not “push.” Convex’s limit model is around active backend deployments/environments, and the practical ceiling for my current usage is high enough that it feels like I can build without immediately rearranging infrastructure.

That does not make Convex the universal answer. It made Convex the right move for this specific product at this specific stage.

The mistake I avoided

The tempting migration plan is the obvious one: export data from Supabase, import it into Convex, switch the environment variables, and test everything.

That is also how you turn a manageable migration into a pile of ambiguous failures.

When everything changes at once, you do not know whether a bug came from the schema, the data transform, the auth session, a missing environment variable, a production-only callback URL, a stale deployment, a browser cookie, or an actual application bug. You just know the app is broken.

The better plan was slower on paper and faster in practice:

  1. Keep production Supabase untouched.
  2. Add Convex locally first.
  3. Preserve the existing UI contracts.
  4. Move reads in slices.
  5. Move writes in slices.
  6. Smoke test each slice in the real browser.
  7. Cut production traffic over only after the fallback path was still intact.
  8. Remove fallbacks after the Convex path was actually carrying the app.

That sequence is the whole lesson.

Keep the old IDs alive

The single most important technical choice was preserving the old Supabase row shapes at the UI boundary.

The app already had URLs, server actions, modals, forms, and internal state built around fields like id, organization_id, repo_id, and entry_id. Convex has its own document IDs, and those IDs are not the same thing. If I had let Convex document IDs leak into the React layer immediately, the migration would have become a UI rewrite at the same time as a database rewrite.

So the adapter layer kept Convex document IDs internal and mapped data back into the old row shape. The UI kept thinking in legacy IDs. The new backend could change underneath it.

That sounds like ceremony, but it is the reason the migration stayed bounded. The app did not need to know the storage layer had changed until we were ready to clean up the old types.

Environment variables are operational design

The other big lesson was that flags are not just developer convenience. They are the migration control plane.

We used flags for things like:

  • local Convex-primary Lab reads
  • production safety guards
  • write freeze behavior
  • admin-only Lab access
  • app-secret protected ingest paths

The exact names are not important. The structure is.

The flags let us ask smaller questions:

  • Can Convex render Projects and Organizations locally?
  • Can Convex render Posts without changing the URL?
  • Can the adapter return the same shapes the UI expects?
  • Can writes persist in Convex and survive reload?
  • Can GitHub ingest write to Convex without breaking the existing source flow?
  • Can production fall forward while Supabase data remains untouched?

Each flag turned one scary migration into a series of reversible checks.

The cutover was really a stack of smaller cutovers

The first real slice was simple: refresh local Convex from local Supabase and make Projects and Organizations render from Convex. That proved the schema, import path, and legacy ID mapping without touching the more complex post surfaces.

Then the post list moved.

Then access checks.

Then platform drafts and events.

Then comments, mentions, notifications, voice profile data, feedback, memories, screenshots, and GitHub ingest state.

Then auth moved away from Supabase.

Then the public waitlist/signup path moved.

This was not glamorous work. It was better than glamorous. It was inspectable.

After each slice, we could look at the app and say what was Convex-backed, what was still Supabase-backed, and what was blocked. That kept the work honest. It also made rollback boring: if a slice failed, we knew where the fallback still lived.

Browser smoke tests caught what tests would not

The automated tests mattered. The migration had mapper tests, access tests, fanout tests, smoke scripts, and build checks.

But the real browser still found issues the test suite would not have caught as quickly.

OAuth is a good example. A stale auth prompt made /lab look like it was failing behind a 404-style guard. That was not a Convex data problem. It was an auth state problem. You only find that cleanly when you test the real deployed URL with the real browser session.

The same thing happened after the main migration looked done. The homepage X signup worked, but the email waitlist form had disappeared. That was not a database correctness bug. It was a product regression. A live smoke test caught it, and the fix was to restore the private email signup path directly into Convex.

That is the kind of thing I want to remember: data migrations do not only break data. They break product affordances around the data.

I kept Supabase production data until the end

One thing I would do again: do not delete the old production data just because the first Convex smoke test passes.

During the cutover, Supabase production stayed available as a rollback source. We did not mutate or wipe it. We moved the application forward, verified Convex in production, then removed runtime Supabase dependencies from the app after the new path was carrying traffic.

That is the difference between migrating and jumping.

When you jump, the old bridge is gone and every bug is urgent. When you migrate, you can keep moving forward while still having a known-good source to compare against.

What I like about Convex for this kind of app

The product I migrated is very AI-assisted. It turns shipped work into launch content, and the engineering loop is also heavily AI-assisted.

Convex fits that style because backend logic, schema, and data access live in TypeScript files the agent can read, edit, test, and deploy. I do not have to split the truth across SQL migrations, RLS policies, dashboard settings, generated client code, and server-side glue as often.

That does not mean SQL is bad. It means the local reasoning loop is different.

For a product like this, I care about:

  • Can an agent understand the backend without guessing dashboard state?
  • Can I test changes locally without touching production?
  • Can I deploy a preview and smoke it against the right backend?
  • Can I keep the product moving without creating a new infra project every time I test an idea?

Convex is strong on those points.

What I would do differently next time

I would decide the auth and email plan earlier.

We correctly treated full auth cutover as its own risk, but the public signup regression proved that “auth” is not one thing. There is session identity, OAuth, email capture, invite flow, admin access, callback URLs, and user-facing copy. They are connected enough to break each other, but different enough that they deserve their own checklist.

I would also write the cleanup checklist before the cutover starts:

  • Which env vars become dead?
  • Which packages should disappear?
  • Which local scripts should be removed?
  • Which docs should say “historical” instead of “current”?
  • Which fallback paths stay for one release and which are removed immediately?

Cleanup is part of the migration, not the victory lap.

My advice if you are moving from Supabase to Convex

Do not start with “replace Supabase.”

Start with a boundary.

Pick one read path. Make Convex return the exact shape your UI already expects. Keep the old IDs visible at the app boundary. Put the new IDs behind the adapter. Add a flag. Smoke it. Then pick one write path.

If the app is important, keep the old production data intact until the Convex path has handled real production behavior. If you need temporary database access for migration, keep it short-lived and do not paste secrets into chats, docs, or issue threads. Use the least exposure that lets you move the data safely.

And do not pretend the migration is done when the database query works. The migration is done when the product still works.

For this migration, the fastest path was not a one-shot rewrite. It was a planned fall-forward migration with boring fallbacks.

That is the part I would copy next time.

QR Code for jessepeplinski.com

Jesse Peplinski

I'm shipping AI-powered products