Skip to content

Migrating from Neon to Layerbase

12 min readNeonPostgresMigrationDatabases

Short version: Neon is plain Postgres, so the database half is a dump and a restore, and the guided flow at layerbase.com/migrate/neon runs it in one pass from a napi_ API key, a couple of minutes for anything under a gigabyte, onto flat pricing that does not meter compute-hours. Tables, indexes, rows, sequences, views, and functions land unchanged, and if you use @neondatabase/serverless you can keep it, because Layerbase Postgres speaks the same HTTP query protocol. Two things do not travel. Neon Auth passwords and sessions live in Stack Auth rather than in your Postgres, so your users keep their profiles and set a new password on first sign-in. Branch topology, autoscaling settings, and point-in-time history are Neon platform state rather than rows, so you recreate the branches you still want. The mistake that costs people an hour is dumping through the pooled endpoint: strip -pooler from the host, or paste the pooled string into the wizard and let it switch for you.

Neon is plain Postgres, which makes the database half of this migration boring in the best way: a dump and a restore, and your tables, indexes, and rows land on the other side unchanged. The parts worth writing about are the two places people trip. The first is the connection string, because Neon hands most people a pooled endpoint that a straight pg_dump should not run through. The second is Neon Auth, which does not live in your database at all, so it does not travel the way you might assume. This post is the honest version of both.

If you are still deciding whether to leave, that is a different question and I wrote it up separately in Neon is now a Databricks product. Should you still use it?. This post assumes you have decided, and covers the move.

Just want it done? Start at layerbase.com/migrate/neon. You sign in, the wizard opens with Neon already selected, and it copies your schema and data in one pass: read-once, nothing written back to Neon. If you only have a pooled connection string, paste it anyway and it switches to the direct endpoint for you. The rest of this post is that same migration explained step by step, plus the manual path.

Contents

What moves and what does not

Be clear about scope before you start.

Moves automatically:

  • Your public schema and any other application schemas: every table, index, constraint, and row.
  • Sequences, views, and functions that live in your own schemas.
  • The neon_auth.users_sync table, if you use Neon Auth. It is a normal table in your database, so it copies like any other. Read the Neon Auth section before you rely on that, though, because the passwords are not in it.

Does not move:

  • Neon Auth credentials and sessions. Passwords and active sessions live in Stack Auth, the hosted service behind Neon Auth, not in your Postgres. There is no table to copy them from, so they cannot come across. Your users keep their profiles and set a new password on first sign-in.
  • Neon-specific control-plane features: your branch topology, compute autoscaling settings, and the point-in-time restore history are Neon platform state, not database rows. The data on your primary branch moves; the branching structure is something you recreate (which is cheap, see below). The restore history does not travel, but the capability exists on the other side: an always-on Postgres database on the Pro plan archives its write-ahead log continuously and restores to any timestamp, into a new database rather than over the top of the live one. It starts a fresh history the day you switch it on.

If your app is a Postgres database with tables and data, the whole thing moves. Auth is the part that needs a plan, and it needs one whether you migrate or not, because Neon Auth was always a separate hosted service bolted onto the database.

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 pooled-endpoint problem automatically.

Step 1: Start the migration

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

Paste a Neon API key (starts with napi_, from the Neon console under Account Settings, then API Keys). Layerbase uses it to list your projects so you can pick the one to move. The key is used for the copy and is not stored afterward.

Step 2: Let it copy

Layerbase provisions a Postgres instance and runs the dump and restore for your schemas. For a database under 1GB this is a couple of minutes. You end up with a running Layerbase Postgres holding your tables and data, on a plan whose price does not move with compute-hours.

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 a standard, direct Postgres URL with TLS. Your existing drivers and ORMs take it as-is.

The connection string gotcha

This is the one that costs people an hour, so it is worth calling out on its own.

Neon gives you two connection strings, and by default the console shows the pooled one. You can tell them apart by the host: the pooled endpoint has -pooler in it (ep-cool-name-123456-pooler.region.aws.neon.tech), the direct one does not. The pooled endpoint runs through PgBouncer in transaction mode, which is great for serverless apps and wrong for a migration: pg_dump opens session-level constructs that transaction pooling does not support, and the dump either errors or comes out incomplete.

The built-in flow handles this for you. If you paste a pooled string, Layerbase rewrites it to the direct endpoint before copying, so you do not have to think about it. If you drive the migration by hand (see The manual path), strip the -pooler from the host yourself and dump against the direct endpoint.

Neon Auth: the honest part

If you never turned on Neon Auth, skip this section: you have a plain Postgres database and nothing here applies.

If you did use it, here is the part people get wrong. Neon Auth is Neon's integration of Stack Auth, a hosted authentication service. It syncs a read-only mirror of your user profiles into a neon_auth.users_sync table so you can join against it in SQL, but the actual credentials, the password hashes and the session state, live inside Stack Auth's service, not in your database. Your database only ever held a copy of the profile fields.

So when you migrate the database, you get the neon_auth.users_sync table like any other table: emails, names, IDs, timestamps. What you do not get is anything you can log a user in with, because that never lived in Postgres to begin with. There is no password hash to copy.

The practical consequence: your users keep their identity (the profile row is right there), but they set a new password the first time they sign in on the new side. That is a re-onboarding, not a data loss, and it is the same thing that would happen if you moved off Neon Auth while staying on Neon. If a no-reset auth migration is what you need, it exists for providers that keep credentials in the database (for example, moving from Supabase carries the bcrypt hashes across because Supabase stores them in auth.users). Neon Auth is architecturally the other kind, and no tool can copy a secret out of a service that does not expose it.

You keep your branching workflow

Branching is the Neon feature people worry about giving up, so to be clear: Layerbase Postgres branches too. If you use Neon branches for preview deploys or to test a migration against production-shaped data, that workflow survives. You migrate your primary branch, then branch it on Layerbase the same way, from the dashboard or the CLI. The branch topology itself does not carry over in the copy (it is platform state, not data), so you recreate the branches you still want, which is a few clicks per branch and usually a shorter list than what accumulated on Neon.

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 uses Neon's serverless driver (@neondatabase/serverless) over HTTP, keep it: Layerbase Postgres speaks the same HTTP query protocol, so neon(process.env.DATABASE_URL), @vercel/postgres, Drizzle's neon-http adapter, and the Prisma and Kysely Neon adapters work against the Layerbase connection string unchanged. The driver derives its endpoint from the hostname, so there is nothing to configure. The one thing that does not carry over is interactive transactions through the driver's WebSocket Pool; sql.transaction([...]) batches work, and a standard pg or postgres client over TLS covers anything session-bound. There is no proprietary client to keep, and no proprietary client you are forced to drop.

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

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. If you used Neon Auth, that is also when you wire the new sign-in path, because your app can no longer call Stack Auth's hosted endpoints.

The manual path

If you would rather drive the dump yourself, the standard Postgres move works, with the one caveat from above: dump against the direct endpoint, not the pooler.

bash
pg_dump \
  --no-owner --no-acl \
  "postgresql://<user>:<password>@ep-cool-name-123456.region.aws.neon.tech/dbname?sslmode=require" \
  > neon-dump.sql

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

Note the host in the source string has no -pooler in it. If yours does, remove it. --no-owner --no-acl strips ownership and grants that would otherwise break the restore on a database with different roles.

FAQ

How much downtime does this take?

The copy itself is read-only against a live Neon project, so nothing goes down while it runs. The cutover is one environment variable and a deploy. The sane sequence is copy, point staging at the new database, compare row counts with the pg_stat_user_tables query above, then ship the env var change. Writes that land on Neon after the copy started will not be on the new side, so either run the copy during a quiet window or take a short write pause at the flip.

What copies across, and what does not?

Every application schema copies: tables, indexes, constraints, rows, sequences, views, and functions, plus neon_auth.users_sync if you have it, because that is an ordinary table. What does not copy is anything that was never a row to begin with. Branch topology, compute autoscaling settings, point-in-time restore history, and Neon Auth credentials are all platform state living outside your Postgres.

Will my users have to sign up again if I use Neon Auth?

No, but they will set a new password once. The profile rows are in neon_auth.users_sync and come across with everything else, so identities, emails, and IDs are intact. The password hashes and sessions are inside Stack Auth, the hosted service behind Neon Auth, and there is no table holding them, so nothing can copy them. That is a re-onboarding rather than a data loss, and you would hit the same wall moving off Neon Auth while staying on Neon.

Do I give up branching?

No. Layerbase Postgres branches, so the preview-deploy and test-a-migration-against-real-shaped-data workflow survives the move, from the dashboard or the CLI. What you lose is the existing topology, since branches are platform state rather than data. You migrate the primary branch and recreate the branches you still want, which is usually a shorter list than what had accumulated on Neon.

Do I give up point-in-time restore?

No, though you do start a new history. An always-on Postgres database on Layerbase, on the Pro plan, archives its write-ahead log continuously and restores to any timestamp the archive covers, and the restore produces a new database with its own connection string rather than writing over the one you still need. What does not come across is the existing restore window, because that history is Neon platform state rather than rows in your database, so the clock starts when you enable archiving on the new side. How it works and the drill that proved it has the details.

Why did my own pg_dump come out empty or incomplete?

Almost certainly the pooler. Neon's console shows the pooled endpoint by default, and it has -pooler in the hostname. That endpoint runs PgBouncer in transaction mode, which does not support the session-level constructs pg_dump relies on. Dump against the direct endpoint instead, which is the same host with -pooler removed. The built-in flow rewrites this for you, so it only bites on the manual path.

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 before you touch production.

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

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

The move off Neon is a dump, a restore, and an env var, on flat pricing that does not meter your compute. The database is the easy part and it moves cleanly. The two things to get right are dumping against the direct endpoint, not the pooler, and being honest with yourself about Neon Auth: the profiles come with you, the passwords were never yours to copy, and your users set a new one once. If you are still weighing the decision itself, the case for leaving Neon is the companion to this how-to.