Skip to content
Discord Get Started

Migrate from Neon

This guide walks through migrating a PostgreSQL database from Neon to DB9. The process uses standard PostgreSQL tooling (pg_dump for export, psql for import).

  • SQL compatibility — DB9 supports the same DML, DDL, joins, CTEs, window functions, and subqueries you use in Neon. Most queries work without changes.
  • PostgreSQL drivers — Any driver that connects via pgwire (node-postgres, psycopg, pgx, JDBC) works with DB9.
  • ORM compatibility — Prisma, Drizzle, SQLAlchemy, TypeORM, Sequelize, Knex, and GORM are tested and supported.
  • Data types — Common types (TEXT, INTEGER, BIGINT, BOOLEAN, TIMESTAMPTZ, UUID, JSONB, arrays, vectors) work identically.
AreaNeonDB9
Connection stringpostgresql://user:pass@ep-*.neon.tech/dbnamepostgresql://<tenant_id>.<role>@pg.db9.io:5433/postgres
Connection poolingBuilt-in PgBouncer (transaction mode)No built-in pooler — use application-side pooling
BranchingCopy-on-write, instant for any sizeFull data copy, async (seconds to minutes)
ComputeAutoscaling, scale-to-zeroFixed per-database, always on
Serverless driver@neondatabase/serverless (HTTP/WebSocket)Standard pgwire + browser HTTP scoped support (phase-1)
Extensions40+ community extensionsFixed built-in set: http, vector, fs9, pg_cron, embedding, uuid-ossp, parquet, zhparser — plus hstore, pgcrypto and plpgsql as metadata-only shims
ReplicationLogical replication supportedLogical decoding from replication slots; no CREATE SUBSCRIPTION
Row-level securitySupportedSupported (CREATE POLICY, ENABLE / FORCE ROW LEVEL SECURITY)
Table partitioningSupportedPARTITION BY RANGE on a timestamp or timestamptz key only
LISTEN/NOTIFYSupportedSupported over pgwire (delivered on commit); the stateless HTTP SQL API can NOTIFY, but a LISTEN there is accepted and never receives anything
Port54325433
Database nameCustom (e.g., neondb)Always postgres

Review the Compatibility Matrix for the full list of supported and unsupported features.

  • Access to your Neon database (direct/unpooled connection string)
  • pg_dump installed locally (comes with PostgreSQL client tools)
  • DB9 CLI installed: curl -fsSL https://db9.ai/install | sh
  • A DB9 account: db9 create --name my-app to create your target database
  1. Export from Neon

    Use pg_dump with Neon’s direct (unpooled) connection string. Do not use the pooled connection — pg_dump requires a direct connection.

    Schema and data (plain SQL format)

    Terminal
    pg_dump --no-owner --no-privileges --no-comments \
    "postgresql://user:pass@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require" \
    > export.sql

    Schema only

    Terminal
    pg_dump --schema-only --no-owner --no-privileges \
    "postgresql://user:pass@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require" \
    > schema.sql

    Flags explained:

    • --no-owner — omits ALTER ... OWNER TO statements that reference Neon-specific roles
    • --no-privileges — omits GRANT/REVOKE statements
    • --no-comments — omits COMMENT ON statements that may reference Neon internals

    Use plain SQL format (default): the cleanup checks in step 2 read and edit the file as text, which a custom (-Fc) or directory (-Fd) archive is not.

  2. Clean the Export

    The pg_dump output may contain statements that DB9 does not support. Remove or comment out:

    • CREATE EXTENSION for extensions DB9 does not have — remove any CREATE EXTENSION whose name is not one of http, uuid-ossp, fs9, pg_cron, parquet, zhparser, vector, embedding. Any other name raises 42704 extension is not available and stops the import there — except hstore, which DB9 accepts. Remove it anyway: DB9 does not support hstore values, and while it is registered pg_dump cannot dump your DB9 database. Two more names, pgcrypto and plpgsql, are accepted and can stay — but read the caution below before you rely on either.
    • Schema-qualified vector names — keep vector, but rewrite how the dump names it rather than removing it. pg_dump writes the type, its operator classes and its operators as public.vector(1536), public.vector_cosine_ops and OPERATOR(public.<=>), which DB9 rejects — the type with 42704 type "public.vector" does not exist — so that table and its rows do not load. Strip the qualifier in schema.sql, the file you import below (and in export.sql if you load that in one pass): sed -E -i.bak -e 's/(^|[^[:alnum:]_])public\.vector([^[:alnum:]_]|_(l2|ip|cosine)_ops|$)/\1vector\2/g' -e 's/OPERATOR\(public\.(<->|<#>|<=>)\)/\1/g' schema.sql — see Migrate from PostgreSQL for what it changes.
    • CREATE PUBLICATION / CREATE SUBSCRIPTION — DB9 cannot subscribe to logical replication (CREATE SUBSCRIPTION fails with 42601), and the source’s publications belong to its own replication setup. Remove both.
    • Table partitioning — DB9 partitions only by RANGE on a timestamp or timestamptz key, and it cannot parse the ALTER TABLE ... ATTACH PARTITION statements pg_dump writes for each partition (42601). Recreate a qualifying table by hand as described in Migrate from PostgreSQL, or merge its partitions into one table.
    • Advisory lock calls — pg_advisory_lock(), pg_try_advisory_lock().
    • Custom types with WHILE loops in PL/pgSQL — DB9 supports basic PL/pgSQL, including CONTINUE, dynamic EXECUTE, exception handling and nested BEGIN ... END blocks, but not WHILE or FOREACH loops, or cursors — see the compatibility matrix.
    • Locale settings — DB9 accepts and ignores locale parameters from pg_dump, so these are safe to leave in.

    A quick way to identify issues:

    Terminal
    # Check for unsupported extensions
    grep "CREATE EXTENSION" export.sql
    # Check for partitioning
    grep -i "PARTITION" export.sql
    # Check for replication
    grep -i "PUBLICATION\|SUBSCRIPTION" export.sql

    Row-level security

    Keep your row-level security statements. ENABLE / FORCE ROW LEVEL SECURITY and CREATE POLICY import from a pg_dump file, and DB9 enforces the policies for roles that are not the table owner. See Row-Level Security. Two things need attention:

    • Roles. pg_dump does not export roles, and a policy that names one (CREATE POLICY ... TO app_user) fails with 42704 role "app_user" does not exist. Create each role in DB9 after step 3 and before step 4. For example: psql "$(db9 db connect my-app --output quiet)" -c "CREATE ROLE app_user LOGIN PASSWORD '<password>'". --no-privileges also leaves out the grants, so grant those roles their table and sequence privileges again after the import.
    • Tables and policies written for the Neon Data API. Neon’s RLS setup uses auth.user_id() (from the pg_session_jwt extension) and the authenticated and anonymous roles. A column default DEFAULT (auth.user_id()) fails CREATE TABLE with 42883 function auth.user_id() does not exist, so the table is not created at all. Remove that default before importing. DB9 has no anonymous role, so a policy TO anonymous fails with 42704. A policy that calls auth.user_id() imports cleanly, then fails with 3F000 schema "auth" does not exist whenever authenticated queries the table. That is the role signed-in Browser SDK requests and functions run as. Rewrite these policies, or move the checks into your application.
  3. Create the DB9 Database

    Terminal
    # Create a new database
    db9 create --name my-app --show-connection-string

    This returns immediately with the connection string and credentials. Save them for your application config.

  4. Import into DB9

    Import over pgwire with psql in two passes: schema.sql from step 1 while the tables are still empty, then the data streamed straight from Neon. Run the step 2 checks against schema.sql, remove what they flag and apply the vector rewrite before importing it.

    Terminal
    # Import the schema, with the statements flagged in step 2 removed
    psql "$(db9 db connect my-app --output quiet)" -f schema.sql
    # Then stream the data from Neon into DB9
    pg_dump --data-only --no-owner \
    "postgresql://user:pass@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require" \
    | psql "$(db9 db connect my-app --output quiet)"

    DB9 supports COPY in CSV and TEXT formats over pgwire. psql keeps going after an error, so scan its output for ERROR: lines.

  5. Update Your Application

    Connection string

    Replace the Neon connection string with DB9’s:

    Diff
    DATABASE_URL=postgresql://user:pass@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require
    DATABASE_URL=postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres?sslmode=require

    Key differences:

    • Username: DB9 uses <tenant_id>.<role> format (e.g., a1b2c3d4e5f6.admin)
    • Port: 5433 (the PostgreSQL default 5432 works too)
    • Database: Always postgres
    • Host: pg.db9.io (not region-specific endpoints)

    Neon serverless driver

    If you use @neondatabase/serverless, replace it with a standard PostgreSQL driver:

    Diff
    import { neon } from '@neondatabase/serverless';
    const sql = neon(process.env.DATABASE_URL);
    const result = await sql`SELECT * FROM users`;
    import pg from 'pg';
    const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
    const result = await pool.query('SELECT * FROM users');

    DB9 uses standard pgwire (TCP), so pg (node-postgres), psycopg, pgx, and other standard drivers work without modification.

    Connection pooling

    Neon provides built-in PgBouncer. DB9 does not include a connection pooler. If your application opens many connections, configure pooling at the application level:

    TypeScript
    // node-postgres pool
    const pool = new pg.Pool({
    connectionString: process.env.DATABASE_URL,
    max: 10, // DB9 handles concurrent connections well
    idleTimeoutMillis: 30000,
    });

    For ORMs, see the integration guides: Prisma, Drizzle, SQLAlchemy.

    Edge Runtime

    If you run code in edge/serverless environments (Cloudflare Workers, Vercel Edge Functions) that relied on Neon’s HTTP driver, move general database query workloads to a Node.js runtime over pgwire. DB9 has browser HTTP scoped support (phase-1), but does not provide full HTTP/WebSocket SQL parity with Neon’s serverless driver.

    See the Next.js guide for patterns that work with both Server Components and API routes.

  6. Validate

    Check schema

    Terminal
    db9 db dump my-app --ddl-only

    Compare the output with your original schema to confirm all tables, indexes, and constraints were created.

    Check row counts

    Run a count on your key tables to verify data was imported:

    Terminal
    db9 db sql my-app -q "SELECT count(*) FROM users"
    db9 db sql my-app -q "SELECT count(*) FROM orders"

    Compare row counts against the source Neon database.

    Run your test suite

    The most reliable validation is running your application’s existing test suite against the DB9 database. Update DATABASE_URL in your test environment and run:

    Terminal
    DATABASE_URL="$(db9 db connect my-app --output quiet)" npm test

    Check for unsupported features

    If your tests fail, check these common differences:

    • SERIALIZABLE isolation — DB9 does not implement SERIALIZABLE. Requesting it on the wire protocol raises a WARNING and silently downgrades the transaction to REPEATABLE READ, so it will not fail loudly. Audit transactions that relied on serializability and add explicit SELECT ... FOR UPDATE locks or unique constraints. At the downgraded level, the transaction that loses a SELECT ... FOR UPDATE race fails at COMMIT with 40001 rather than at the statement, so make sure your retry logic covers COMMIT as well; a unique-constraint conflict fails the INSERT with 23505, as in PostgreSQL. See Transactions
    • LISTEN/NOTIFY — supported over a direct pgwire connection; the stateless HTTP SQL API can NOTIFY, but a LISTEN there is accepted and never receives anything
    • Advisory locks — available, but coordination is node-local (not cross-process/global). For strict row-level coordination semantics, use SELECT ... FOR UPDATE.
    • Row-level security — policies apply to roles other than the table owner, and the admin role is a superuser that bypasses them, so test each policy as the role your application connects with. DB9 also does not check a policy’s expression when you create it: a missing column only fails when a role subject to the policy queries the table. See Row-Level Security

If you need to revert:

  1. Your Neon database is unchanged — switch DATABASE_URL back to the Neon connection string.
  2. If you need to export data created in DB9 back to Neon:
Terminal
# Export from DB9
db9 db dump my-app -o db9-export.sql
# Import to Neon (use direct/unpooled connection)
psql "postgresql://user:pass@ep-cool-name-123456.us-east-2.aws.neon.tech/neondb?sslmode=require" \
-f db9-export.sql

The db9 db dump command outputs plain SQL. Its limit of 50,000 rows or 16 MB applies to the whole database, not to each table: a larger database is refused with Error 413 and no file is written. For larger databases, use pg_dump 17 or newer instead — see the PostgreSQL guide’s rollback plan for the command, the cases it cannot dump, and the extra step for vector columns — or use psql to stream individual tables with COPY.

  • No zero-downtime migration — DB9 cannot subscribe to logical replication (CREATE SUBSCRIPTION is not supported), so you cannot stream changes from Neon into it in real time. Plan a maintenance window or accept a brief cutover period.
  • Extension gaps — If your Neon database uses extensions not in DB9’s built-in set (e.g., PostGIS, pg_trgm, ltree), those features will not be available and CREATE EXTENSION raises 42704 at import time. pgcrypto is the exception worth singling out: DB9 accepts the statement but registers no functions, so crypt() and gen_salt() fail only when something calls them. Check your CREATE EXTENSION statements.
  • Import with psql, not db9 db sql -f — db9 db sql -f cannot run a pg_dump file, with or without --direct, and the HTTP SQL API it uses by default also rejects requests over about 2 MB and gives up after about 15 seconds. Import over pgwire, schema first.
  • Branching model — Neon branches are copy-on-write and instant. DB9 branches are full copies and take longer for large databases. Adjust CI workflows that depend on instant branching.
  • Autoscaling — Neon can scale compute to zero when idle. DB9 databases are always on. This affects cost for rarely-used databases.