Skip to content

Migrating from Supabase to Layerbase

10 min readSupabasePostgresMigrationAuthenticationDatabases

Short version: you land on a managed Postgres, and the built-in wizard copies your public schema, your data, and your auth.users rows in one read-only pass, which is a couple of minutes for a database under 1GB. The bcrypt password hashes are copied byte for byte and the user UUIDs are preserved, so nobody resets a password and every user_id foreign key still lines up. What does not travel: Supabase Storage, edge functions, realtime subscriptions, and active sessions, so plan those separately. The one piece of real work left is login itself, because GoTrue's hosted endpoints do not come with you and you wire a credential route against the migrated table on the new side.

The hard part of leaving Supabase was never the database. A Postgres dump and restore is a solved problem. The hard part was auth. Your users live in auth.users, their passwords are hashed in a schema you do not own, and the usual advice is "leave auth on Supabase" or "make everyone reset their password on the new provider." Both are bad answers.

Layerbase now copies your Supabase Auth users across with their password hashes intact. That is the part that usually forces a mass password reset, and it does not here: the credentials survive the move, so you skip the "we emailed everyone a reset link and lost a third of them" step. What you still own is the login wiring on the new side, and this post is honest about that too. It covers the whole move: the data, the users, and what you actually have to change in your app.

Just want it done? Start at layerbase.com/migrate/supabase. You sign in, the wizard opens with Supabase already selected, and it copies your schema, data, and auth users in one pass: read-once, nothing written back to Supabase. The rest of this post is that same migration explained step by step, plus the manual path if you would rather run it yourself.

Contents

What moves and what does not

Be clear about scope before you start.

Moves automatically:

  • Your public schema: every table, index, and row.
  • Your auth.users rows, with the bcrypt password hashes copied verbatim. The user IDs (UUIDs) are preserved, so every user_id foreign key in your tables still lines up.

Does not move:

  • Supabase storage. Both the files (the bytes in Supabase's bucket) and the storage tables are a separate migration to S3 or R2. Most teams leave storage in place at first and move just the database and auth.
  • Scheduled jobs. Layerbase does not offer pg_cron yet. Copy your job definitions out of cron.job before switching and reschedule them from your app or CI.
  • Active sessions. Existing JWTs do not carry over, so every user signs in once more after the cutover. They sign in with the same password they already have, so this is a re-login, not a reset.
  • Edge functions and realtime subscriptions. These are Supabase services, not database rows. If you use them, plan a replacement (API routes for functions, Postgres LISTEN/NOTIFY or polling for realtime).

If your app is a Postgres database plus email/password auth, the whole thing moves. The extras are the parts that need a plan.

The migration, step by step

The fastest path is the built-in migration flow. It runs the dump and restore for you and handles the auth import in the same pass.

Step 1: Start the migration

In the Layerbase dashboard, click New database and choose Migrating from another platform, then pick Supabase. (You can also open the Migrate tab on an existing blank database.)

Supabase does not expose your database password through its API, so you give Layerbase two things:

  • A Supabase access token (Account, then Access Tokens, starts with sbp_). Layerbase uses it to list your projects so you can pick one.
  • The database password for the project you chose, pasted once.

Step 2: Let it copy

Layerbase provisions a Postgres instance, runs the dump and restore for your application schema, and then detects the auth.users table. When it finds auth users, it offers to import them. Say yes.

For a database under 1GB this is a couple of minutes. You end up with a running Layerbase Postgres holding your tables, your data, and your users.

Step 3: Grab the connection string

Open the new database in the dashboard and copy its connection string. It looks like:

text
postgresql://layerbase:<password>@your-host.cloud.layerbase.dev:5432/app?sslmode=require

That is the value your app needs.

How the auth users come across

This is the part that used to keep people on Supabase, so it is worth being precise about what happens.

Supabase's auth schema is owned by supabase_auth_admin and depends on pgsodium and the GoTrue service. You cannot blind-restore it onto another Postgres; the restore fails on roles and functions that do not exist on the target. So Layerbase does not try to. Instead it copies the rows that matter into a recognized, user-owned auth schema on the new database:

  • auth.users: id, email, and encrypted_password (the bcrypt hash, copied byte for byte and never re-hashed), plus profile and lifecycle fields like email_confirmed_at, last_sign_in_at, and banned_until.
  • auth.identities: the OAuth provenance rows (Google, GitHub) on a best-effort basis. A missing or odd-shaped identities table never fails the user import.

Because the hash is copied untouched, the credential is still valid: an auth layer that bcrypt-checks against auth.users will accept the user's existing password. You are not forced to reset anyone. The catch is that you have to provide that auth layer, which is the next section.

Once the migration finishes, those users show up in the Auth console in your dashboard. From there you can list them, create one, reset a password, ban a user, or delete one. The console manages the schema for you, so you are not writing SQL against auth.users by hand. Note that this is a management surface, not a login endpoint.

Pointing your app at the new database

The database swap is one environment variable. Change DATABASE_URL to the Layerbase connection string and deploy. If your code talks to Postgres through the Supabase JS client rather than a plain driver, swap it for postgres or pg. See Connect a Postgres database to a Lovable app for the exact client code.

Auth needs more than a connection string, so be clear-eyed about it. Your users and their bcrypt hashes now live in a GoTrue-shaped auth.users table on the new database, and the Auth console manages them. What Layerbase does not do is run a login service for you. Your Supabase app called GoTrue's hosted endpoints, and those are gone, so you wire login on the new side: an auth layer that reads auth.users and bcrypt-checks the submitted password. That is a small credential route (look up the row by email, bcrypt.compare, issue your own session), and the hashes are ready for it.

One thing to keep straight: this is not the same as the Better Auth wizard. That wizard scaffolds its own user and account tables with a different hash, so a freshly scaffolded Better Auth server will not recognize the migrated GoTrue users. Treat "bring existing users" and "scaffold new auth" as two separate choices, not one flow. If you want a turnkey library experience instead of a hand-rolled route, you are choosing a fresh auth server and re-onboarding users, not carrying the hashes.

Verify before you cut over. Run the app against the new database in staging and spot-check row counts against Supabase:

sql
select schemaname, relname, n_live_tup
from pg_stat_user_tables
order by n_live_tup desc;

The counts should match. If they do, flip production by shipping the env var change.

The manual path

If you would rather drive the dump yourself, the standard Postgres move still works.

Open Connect in your Supabase project and copy the Session pooler URI. Keep the project-specific username (postgres.<project-ref>) and host from that dialog, and replace the password placeholder. The example below uses aws-0-us-east-1; your pooler index and region may differ. Use session mode on port 5432 for pg_dump, not transaction mode on 6543. The default direct host requires IPv6, so the session pooler also works from IPv4-only machines such as Layerbase's migration servers.

When inserting a raw password into the URI, percent-encode special characters in the password component only (for example, @ becomes %40, : becomes %3A, and % becomes %25). Do not encode the whole URI or re-encode a password that is already encoded in a copied URI.

bash
pg_dump \
  --no-owner --no-acl \
  --schema=public \
  "postgresql://postgres.<project-ref>:<password>@aws-0-us-east-1.pooler.supabase.com:5432/postgres" \
  > supabase-dump.sql

psql \
  "postgresql://layerbase:<password>@your-host.cloud.layerbase.dev:5432/app?sslmode=require" \
  < supabase-dump.sql

--no-owner --no-acl strips the Supabase-specific ownership and grants that would otherwise break the restore. The catch is the part above: a hand-run pg_dump cannot bring the auth schema across, so the manual path gives you the data but not the users. If you want the auth import, use the built-in flow.

FAQ

Will my users have to reset their passwords?

No, and that is the point of the auth import. The encrypted_password column comes across as-is, copied byte for byte and never re-hashed, so an auth layer that bcrypt-checks against auth.users accepts the password the user already has. What they do have to do is sign in again once, because existing Supabase JWTs are not valid against your new login route. That is a re-login, not a reset.

What does the wizard actually ask me for?

Two things. A Supabase access token that starts with sbp_, from Account then Access Tokens, which Layerbase uses to list your projects so you can pick one. Then the database password for that project, pasted once, because Supabase deliberately never exposes it through its API. If you would rather skip the token entirely, the wizard also takes the Session pooler connection string from the Connect dialog, with your database password swapped in. Use port 5432, not the transaction-mode pooler on 6543.

When inserting a raw password into the URI, percent-encode special characters in the password component only (for example, @ becomes %40, : becomes %3A, and % becomes %25). Do not encode the whole URI or re-encode a password that is already encoded in a copied URI. In the wizard's separate Database password field, paste the raw password without URI encoding.

How much downtime does the cutover need?

The copy itself needs none: it reads Supabase once and writes nothing back, so your live project keeps serving while it runs. The cutover is the deploy that changes DATABASE_URL, and anything written to Supabase after the copy started stays behind, so do the copy against a quiet database or accept a short write freeze. Verify row counts in staging first, then flip production by shipping the env var.

What happens to Storage, edge functions, and realtime?

They stay where they are. Supabase Storage is both files in a bucket and a set of storage tables, which is its own migration to S3 or R2, and most teams leave it in place while they move the database. Edge functions and realtime are Supabase services rather than database rows, so plan replacements: API routes for functions, and Postgres LISTEN/NOTIFY or polling for realtime.

Can I just use the Better Auth wizard instead of importing users?

You can, but it is a different decision, not a shortcut to the same place. Better Auth scaffolds its own user and account tables with a different hash format, so a freshly scaffolded server will not recognize your migrated GoTrue rows. Pick one: carry the existing hashes and write a small credential route, or start a fresh auth server and re-onboard everyone.

Develop against a local copy

The Layerbase CLI (formerly SpinDB) runs Postgres locally with no Docker and accepts the same dump file, so you can rehearse the whole migration on your machine first.

bash
npm i -g layerbase
lbase create supabase-test --start --connect
psql "$(lbase url supabase-test)" < supabase-dump.sql

Restore the dump locally, point your app at lbase url supabase-test, and confirm everything works before you touch production. When you are ready, create the cloud database and run the real migration.

The move off Supabase is a dump, a restore, and an env var. Your users and their credentials come across instead of getting reset, which takes the worst part of an auth migration off the table. Wiring login against the preserved auth.users table is the work that remains, and it is ordinary work you control.