Skip to content

Migrate Supabase Postgres to Neon

Startup · practitioner · 9 min read · last reviewed 2026-09-13

A Postgres to Postgres runbook: the version match and restore flags that decide whether it works, which extensions survive, how the connection string changes, and the free-tier limit that takes you offline.

TL;DR

  • Create the Neon project on the same Postgres major version as Supabase; new Supabase projects run 17, older ones run 15, and Neon supports 14 through 18.
  • Restore with --no-owner and --no-acl, because Supabase ties ownership and ACLs to auth roles that do not exist on Neon. Grants do not carry over and are re-applied by hand.
  • pgvector, pg_cron, and PostGIS are all supported on Neon. pg_net and pgsodium are not, so Supabase's async HTTP and secrets features need application-side replacements.
  • The anon, authenticated, and service_role roles are a PostgREST construct, so RLS policies reading auth.uid() have no source once the database moves.
  • Exceeding any Neon free limit suspends compute until the next billing month rather than throttling, which means a production app is simply down.

Moving from Supabase Postgres to Neon is a Postgres to Postgres migration, which makes it one of the easier platform moves available. The database itself transfers with pg_dump and pg_restore. What does not transfer is everything Supabase built around the database: the auth and storage schemas, the anon and authenticated roles, and two extensions Neon does not carry. Get the version match and the restore flags right and the rest is mechanical.

Two flags and one version decide whether this works

Almost every failed attempt at this migration comes down to three things, so handle them before you start.

Match the Postgres major version. Create the Neon project on the same major version your Supabase project runs. New Supabase projects are on Postgres 17 while older ones sit on 15. Neon supports 14 through 18. A pg_dump from one major version restoring into another is the standard failure, and it fails late, after you have waited for the dump.

Restore with `--no-owner --no-acl`. Supabase ties object ownership and access control lists to its own auth roles. Those roles do not exist on Neon. Without both flags the restore dies on permissions it cannot grant to users that were never created. Grants do not carry over either, so you re-apply them by hand afterward.

Dump the schemas you actually want. pg_dump takes public by default. That is usually correct here, because the Supabase-specific schemas are backed by services Neon does not run.

pg_dump \
  --no-owner --no-acl \
  --schema=public \
  --format=custom \
  --file=dump.bak \
  "postgresql://postgres:[PASSWORD]@db.[REF].supabase.co:5432/postgres"

pg_restore \
  --no-owner --no-acl \
  --dbname="postgresql://[USER]:[PASSWORD]@ep-xxx.[REGION].aws.neon.tech/neondb?sslmode=require" \
  dump.bak

For a large database, or one that cannot take downtime, Neon documents a logical replication path instead. Use it when the dump and restore window is longer than your tolerance for a read-only period.

What does not come with you

Copying the Supabase-specific schemas is possible and mostly pointless, because the services behind them do not exist on Neon.

SchemaWhat it didAfter the move
authUsers, sessions, password hashesWorth dumping once to extract bcrypt hashes, then discard
storageObject metadata for Supabase StorageReplace the service, migrate the objects separately
realtimeChange subscriptionsNo equivalent; use polling, a queue, or another provider
supabase_functionsEdge function hooksRebuild as application code or serverless functions
vaultEncrypted secretsMove to your platform's secret manager
graphqlThe auto-generated GraphQL APIGone; you already have SQL

The roles go too. anon, authenticated, and service_role are a Supabase and PostgREST construct, not a Postgres feature you can recreate. On Neon your application connects as an ordinary Postgres user. Row level security still works, because RLS is core Postgres, but the mechanism that populated the JWT claims your policies read is gone. If your authorization lives in RLS policies calling auth.uid(), that logic has to move into your application layer. Plan it as its own piece of work, and see Replace Supabase Auth for the option that avoids it entirely.

Extensions: what survives

This is better than most people expect. Neon supports over a hundred extensions, and the three that usually matter all made it:

  • pgvector is supported, so retrieval augmented generation and semantic search continue without change. If you are running embeddings against your Postgres, they keep working.
  • pg_cron is supported, so scheduled jobs survive.
  • PostGIS is supported, so geospatial work survives.

Two do not appear on Neon's supported list and need application-side replacements:

  • pg_net, which Supabase uses for async HTTP calls from SQL. Move those calls into your application or a background worker. This is usually an improvement, because HTTP calls from inside the database are hard to observe and harder to retry.
  • pgsodium, which backs Supabase Vault for encryption and secrets. Move to a dedicated secret manager.

Check your own extension list before you start rather than trusting a general answer:

select extname, extversion from pg_extension order by extname;

Connection strings change shape

Supabase and Neon both offer pooled and direct connections, and they express it differently. On Supabase you get a direct endpoint at db.<ref>.supabase.co:5432, and a shared pooler at aws-<index>-<region>.pooler.supabase.com where port 6543 is transaction mode and 5432 is session mode. The username carries the project reference as postgres.<PROJECT-REF>.

On Neon you insert -pooler into the hostname and keep the same port. So ep-xxx.region.aws.neon.tech becomes ep-xxx-pooler.region.aws.neon.tech, running PgBouncer in transaction mode with a ceiling of 10,000 client connections.

The practical changes to your app config are: swap the host, drop the postgres.<ref> username convention, and drop the 6543 port convention. Keep your prepared-statement workarounds, because transaction-mode pooling still does not support prepared statements on either platform. Whatever you were already setting stays set, whether that is prepare: false, ?pgbouncer=true, or statement_cache_size=0.

What you gain

Two things are better on the other side, and they are worth knowing about before you start so you actually use them.

Branching is deeper than what you are leaving. Supabase branching creates ephemeral projects tied to a Git branch and driven by migrations. Neon branches are instant copy-on-write clones that include the data, create no load on the parent, and support point-in-time branching within a history window. That window runs from six hours on the free plan up to thirty days depending on plan. Restoring a table someone truncated at lunch becomes a branch rather than an incident.

Scale to zero is real. Neon suspends compute after five minutes idle and reactivates within a few hundred milliseconds. Compare that to a free Supabase project, which pauses after a week of inactivity and needs a manual restore. On the free plan you cannot disable scale to zero, and it applies to computes at or below 16 compute units.

The free tier limit that will bite you

Neon's free plan gives 0.5 GB of storage per project, 100 compute-unit hours per project per month, 100 projects, and 10 branches per project. The generosity is in the project count, and the trap is in the failure mode.

Exceeding any free limit, whether that is the compute hours, the storage, or the 5 GB of egress, suspends compute until the next billing month. It does not throttle and it does not bill you a small overage. For a hobby project that is fine. For anything with real users it means the application is down until the calendar turns over, so set up billing before you need it.

The Launch plan is usage-based with no fixed fee, at $0.106 per compute-unit hour and $0.35 per GB-month of storage. For most small production workloads that is a few dollars a month, and it removes the suspension cliff entirely.

Credits, if you qualify

Neon was acquired by Databricks in May 2025, and the startup program moved with it. The Databricks Startup Program, announced in June 2026, offers up to $200,000 in credits across Databricks and Neon for venture-backed companies from pre-seed through Series A.

If you are bootstrapped, that program is closed to you, and the free tier plus the Launch plan is the honest answer. For the wider set of options and which ones accept self-funded teams, see Supabase alternatives compared.

Cut over without a gap

Run both databases at once and keep the switch in your own configuration rather than in DNS.

  1. Restore into Neon and verify counts. Compare row counts per table, not just a successful exit code. A restore can succeed while silently skipping objects it could not own.
  2. Re-apply grants. They did not come across. Check that your application user can do exactly what it needs and nothing more.
  3. Run read traffic against Neon first. Point a replica of your read path at Neon while writes still go to Supabase. Query plans differ between hosts more often than people expect.
  4. Freeze writes, take a final delta, flip the connection string. Keep the window short and announced.
  5. Keep Supabase alive and paid through at least one billing cycle. Rollback is then a connection string change, not a restore from backup.

Do not combine this with an auth migration. Two moving identity systems and two moving databases in the same window means a failure you cannot attribute, and that is the difference between a rollback and an outage.

Key takeaways

  • Two flags and one version number account for most failed attempts at this migration, and all three are decided before you run anything.
  • Copying the auth schema is worth doing exactly once, to extract bcrypt hashes, and then discarding.
  • Moving authorization out of RLS and into your application is the real work; budget for it separately from the data move.
  • Neon branching is a genuine upgrade over Supabase branching because clones include data and support point-in-time recovery within the history window.
  • Set up billing before you need it, because the free tier's failure mode is suspension for the rest of the month, not a small overage charge.
  • Never run the database migration and an auth migration in the same window, because a failure becomes unattributable.

Frequently asked questions

Is this a hard migration?
No, because it is Postgres to Postgres. pg_dump and pg_restore is the whole data move, and your SQL, ORM, and most extensions are unchanged. The difficulty is concentrated in what Supabase built around the database rather than in the database itself.
Which extensions do not survive the move?
pgvector, pg_cron, and PostGIS are all supported on Neon. The two that are not on Neon's supported list are pg_net, which Supabase uses for async HTTP calls from SQL, and pgsodium, which backs Supabase Vault. Both need application-side replacements. Check your own list with a query against pg_extension before you start.
What happens to my row level security policies?
RLS is core Postgres and still works, but the mechanism that fed it is gone. Policies call auth.uid(), which reads claims that PostgREST extracted from a Supabase JWT. On Neon your application connects as an ordinary Postgres user, so there are no JWT-derived roles and the policies have nothing to read. Authorization moves into your application layer.
How does the connection string change?
Neon puts pooling in the hostname rather than the port. You insert -pooler into the endpoint host and keep the port. Drop Supabase's postgres.PROJECT-REF username convention and its 6543 transaction-mode port. Keep your prepared-statement workarounds, because transaction-mode pooling still does not support them on either platform.
What is the catch with Neon's free tier?
The failure mode. Exceeding the 100 compute-unit hours, 0.5 GB of storage, or 5 GB of egress suspends compute until the next billing month. It does not throttle and it does not charge a small overage, so a production application stays down until the calendar turns over. The Launch plan is usage-based with no fixed fee and removes the cliff.

Related

← All Migration guides