Migrate from Railway
Railway provides managed PostgreSQL databases as part of its application hosting platform. Since Railway runs standard PostgreSQL, migration to DB9 uses the same pg_dump / import workflow as any PostgreSQL migration.
For the general PostgreSQL migration guide, see Migrate from PostgreSQL.
What Changes and What Stays the Same
Section titled “What Changes and What Stays the Same”Stays the same
Section titled “Stays the same”- SQL compatibility — DB9 supports the same DML, DDL, joins, CTEs, window functions, and subqueries you use in Railway PostgreSQL. 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.
Changes
Section titled “Changes”| Area | Railway PostgreSQL | DB9 |
|---|---|---|
| Connection string | postgresql://postgres:pass@host.railway.app:port/railway | postgresql://<tenant_id>.<role>@pg.db9.io:5433/postgres |
| Port | Varies per deployment | 5433 |
| Database name | railway (default) | Always postgres |
| Connection pooling | No built-in pooler | No built-in pooler — use application-side pooling |
| Extensions | Most community extensions available | Fixed built-in set: http, vector, fs9, pg_cron, embedding, uuid-ossp, parquet, zhparser — plus hstore, pgcrypto and plpgsql as metadata-only shims |
| Replication | Logical replication supported | Logical decoding from replication slots; no CREATE SUBSCRIPTION |
| Table partitioning | Supported | PARTITION BY RANGE on a timestamp or timestamptz key only |
| LISTEN/NOTIFY | Supported | Supported over pgwire (delivered on commit); the stateless HTTP SQL API can NOTIFY, but a LISTEN there is accepted and never receives anything |
| Deployment coupling | Tight integration with Railway services via $DATABASE_URL | Standalone — connect from any platform |
Review the Compatibility Matrix for the full list of supported and unsupported features.
Prerequisites
Section titled “Prerequisites”- Access to your Railway project and database credentials
- Railway CLI installed (
npm install -g @railway/cli) or connection string from the Railway dashboard pg_dumpinstalled locally (comes with PostgreSQL client tools)- DB9 CLI installed:
curl -fsSL https://db9.ai/install | sh - A DB9 account:
db9 create --name my-appto create your target database
-
Get Your Railway Connection String
Option A: Railway dashboard
Go to your project → PostgreSQL service → Variables tab → copy
DATABASE_URL.Option B: Railway CLI
Terminal railway loginrailway link # Link to your projectrailway variables get DATABASE_URLThe connection string looks like:
postgresql://postgres:password@host.railway.app:12345/railway -
Export from Railway
Use
pg_dumpwith the Railway connection string.Schema and data (plain SQL format)
Terminal pg_dump --no-owner --no-privileges --no-comments \"postgresql://postgres:password@host.railway.app:12345/railway" \> export.sqlSchema only
Terminal pg_dump --schema-only --no-owner --no-privileges \"postgresql://postgres:password@host.railway.app:12345/railway" \> schema.sqlFlags explained:
--no-owner— omitsALTER ... OWNER TOstatements that reference Railway-specific roles--no-privileges— omitsGRANT/REVOKEstatements--no-comments— omitsCOMMENT ONstatements
Use plain SQL format (default): the cleanup checks in step 3 read and edit the file as text, which a custom (
-Fc) or directory (-Fd) archive is not. -
Clean the Export
The
pg_dumpoutput may contain statements that DB9 does not support. Remove or comment out:CREATE EXTENSIONfor extensions DB9 does not have — remove anyCREATE EXTENSIONwhose name is not one ofhttp,uuid-ossp,fs9,pg_cron,parquet,zhparser,vector,embedding. Any other name raises42704 extension is not availableand stops the import there — excepthstore, which DB9 accepts. Remove it anyway: DB9 does not supporthstorevalues, and while it is registeredpg_dumpcannot dump your DB9 database. Two more names,pgcryptoandplpgsql, are accepted and can stay — but read the caution below before you rely on either.- Schema-qualified
vectornames — keepvector, but rewrite how the dump names it rather than removing it.pg_dumpwrites the type, its operator classes and its operators aspublic.vector(1536),public.vector_cosine_opsandOPERATOR(public.<=>), which DB9 rejects — the type with42704 type "public.vector" does not exist— so that table and its rows do not load. Strip the qualifier inschema.sql, the file you import below (and inexport.sqlif 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 SUBSCRIPTIONfails with42601), and the source’s publications belong to its own replication setup. Remove both.- Table partitioning — DB9 partitions only by
RANGEon atimestamportimestamptzkey, and it cannot parse theALTER TABLE ... ATTACH PARTITIONstatementspg_dumpwrites for each partition (42601). Recreate a qualifying table by hand as described in Migrate from PostgreSQL, or merge its partitions into one table.
A quick way to identify issues:
Terminal # Check for unsupported extensionsgrep "CREATE EXTENSION" export.sql# Check for partitioninggrep -i "PARTITION" export.sql# Check for replicationgrep -i "PUBLICATION\|SUBSCRIPTION" export.sqlRow-level security
Keep your row-level security statements.
ENABLE/FORCE ROW LEVEL SECURITYandCREATE POLICYimport from apg_dumpfile, and DB9 enforces the policies for roles that are not the table owner. See Row-Level Security. One thing needs attention:- Roles.
pg_dumpdoes not export roles, and a policy that names one (CREATE POLICY ... TO app_user) fails with42704 role "app_user" does not exist. Create each role in DB9 after step 4 and before step 5. For example:psql "$(db9 db connect my-app --output quiet)" -c "CREATE ROLE app_user LOGIN PASSWORD '<password>'".--no-privilegesalso leaves out the grants, so grant those roles their table and sequence privileges again after the import.
-
Create the DB9 Database
Terminal db9 create --name my-app --show-connection-stringThis returns immediately with the connection string and credentials. Save them for your application config.
-
Import into DB9
Import over pgwire with
psqlin two passes:schema.sqlfrom step 2 while the tables are still empty, then the data streamed straight from Railway. Run the step 3 checks againstschema.sql, remove what they flag and apply thevectorrewrite before importing it.Terminal # Import the schema, with the statements flagged in step 3 removedpsql "$(db9 db connect my-app --output quiet)" -f schema.sql# Then stream the data from Railway into DB9pg_dump --data-only --no-owner \"postgresql://postgres:password@host.railway.app:12345/railway" \| psql "$(db9 db connect my-app --output quiet)"DB9 supports
COPYin CSV and TEXT formats over pgwire.psqlkeeps going after an error, so scan its output forERROR:lines. -
Update Your Application
Connection string
Replace the Railway connection string with DB9’s:
Diff DATABASE_URL=postgresql://postgres:password@host.railway.app:12345/railwayDATABASE_URL=postgresql://a1b2c3d4e5f6.admin@pg.db9.io:5433/postgres?sslmode=requireKey differences:
- Username: DB9 uses
<tenant_id>.<role>format (e.g.,a1b2c3d4e5f6.admin) - Port: 5433
- Database: Always
postgres - Host:
pg.db9.io
Railway service variables
If your Railway services reference
$DATABASE_URLas a shared variable, update the variable in each service that connects to the database, or set a new variable pointing to DB9 and update your code to use it.Connection pooling
Railway does not include a built-in connection pooler, so your application likely already handles pooling. Verify your pool settings work with DB9:
TypeScript const pool = new pg.Pool({connectionString: process.env.DATABASE_URL,max: 10,idleTimeoutMillis: 30000,});For ORMs, see the integration guides: Prisma, Drizzle, SQLAlchemy.
- Username: DB9 uses
-
Validate
Check schema
Terminal db9 db dump my-app --ddl-onlyCompare the output with your original schema to confirm all tables, indexes, and constraints were created.
Check row counts
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 Railway database.
Run your test suite
Terminal DATABASE_URL="$(db9 db connect my-app --output quiet)" npm testCheck 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
WARNINGand silently downgrades the transaction to REPEATABLE READ, so it will not fail loudly. Audit transactions that relied on serializability and add explicitSELECT ... FOR UPDATElocks or unique constraints. At the downgraded level, the transaction that loses aSELECT ... FOR UPDATErace fails atCOMMITwith40001rather than at the statement, so make sure your retry logic coversCOMMITas well; a unique-constraint conflict fails theINSERTwith23505, as in PostgreSQL. See Transactions - LISTEN/NOTIFY — supported over a direct pgwire connection; the stateless HTTP SQL API can
NOTIFY, but aLISTENthere is accepted and never receives anything - Advisory locks — available, but coordination is node-local. For strict row-level coordination, use
SELECT ... FOR UPDATE
- SERIALIZABLE isolation — DB9 does not implement SERIALIZABLE. Requesting it on the wire protocol raises a
Rollback Plan
Section titled “Rollback Plan”If you need to revert:
- Your Railway database is unchanged — switch
DATABASE_URLback to the Railway connection string. - If you need to export data created in DB9 back to Railway:
# Export from DB9db9 db dump my-app -o db9-export.sql
# Import to Railwaypsql "postgresql://postgres:password@host.railway.app:12345/railway" \ -f db9-export.sqlThe 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.
Caveats
Section titled “Caveats”- No zero-downtime migration — DB9 cannot subscribe to logical replication (
CREATE SUBSCRIPTIONis not supported), so you cannot stream changes from Railway into it in real time. Plan a maintenance window or accept a brief cutover period. - Extension gaps — If your Railway database uses extensions not in DB9’s built-in set (e.g.,
PostGIS,pg_trgm,ltree), those features will not be available andCREATE EXTENSIONraises42704at import time.pgcryptois the exception worth singling out: DB9 accepts the statement but registers no functions, socrypt()andgen_salt()fail only when something calls them. Check yourCREATE EXTENSIONstatements. - Import with
psql, notdb9 db sql -f—db9 db sql -fcannot run apg_dumpfile, 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. - Railway integration loss — Railway’s tight coupling between services (automatic
DATABASE_URLinjection, private networking) will not apply to DB9. Update each service’s environment variables manually.
Next Pages
Section titled “Next Pages”- Compatibility Matrix — full list of supported and unsupported PostgreSQL features
- Connect — connection string format and authentication options
- Migrate from PostgreSQL — general PostgreSQL migration path
- Production Checklist — deployment readiness