Migrating from Supabase to Layerbase

SupabasePostgresMigrationAuthenticationDatabases

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.

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.
  • 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:

bash
pg_dump \
  --no-owner --no-acl \
  --schema=public \
  "postgresql://postgres:<password>@db.<project>.supabase.co: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.

Develop against a local copy

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 spindb
spindb create supabase-test --start --connect
psql "$(spindb url supabase-test)" < supabase-dump.sql

Restore the dump locally, point your app at spindb 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.

Something not working?