Migrate Heroku Postgres to a New Host Without Surprises
To migrate Heroku Postgres, you take a consistent copy of the database while writes are stopped, restore it on the new host, check it, and point your app's DATABASE_URL at the new server. Because Heroku Postgres does not support logical replication to external databases, the move is a one-time copy, so the main thing to plan is the length of the write freeze.
This guide covers both routes: StitchDB's import wizard (paste a connection string, it does the copy) and the classic pg:backups download plus pg_restore. Both work for any Postgres destination; the StitchDB-specific steps are marked.
#What makes it different to migrate Heroku Postgres
Two facts shape every Heroku Postgres migration:
- No logical replication out. Heroku's help center states that Heroku Postgres does not support logical replication to external instances, and customers do not get the superuser or replication role. That rules out tools that stream changes until a cut-over. You copy once, with writes stopped.
- Credentials can rotate. Heroku manages the
DATABASE_URLand can rotate it. Read the current value right before you start, not from a note you made last week.
So the plan is: measure, rehearse, freeze, copy, verify, switch.
#Step 0: Measure the database
Find out how big the database is and how long a copy takes. Run this against Heroku:
select pg_size_pretty(pg_database_size(current_database())) as total;
select relname as table, n_live_tup as approx_rows,
pg_size_pretty(pg_total_relation_size(relid)) as size
from pg_stat_user_tables
order by pg_total_relation_size(relid) desc
limit 15;Heroku's own backups are logical dumps that skip index data (indexes are rebuilt on restore), so the dump is often much smaller than the database. Restore time, though, includes rebuilding every index. Your rehearsal is the only reliable number.
Also check the extensions you use:
select extname, extversion from pg_extension order by 1;Make sure the destination offers each one. On StitchDB the extensions page lists what is available.
#Route A: StitchDB's Import from Postgres
This is the shortest path if StitchDB is your destination. The import connects to Heroku from your new project's server and copies everything in your schemas: tables and data, views, functions, triggers, types, policies and sequences.
Create a new, empty StitchDB project. The import refuses a project that already has tables. Pick the region carefully: StitchDB's data centers are in Germany and Finland, and the region cannot be changed later.
Get the current Heroku connection string:
Terminalheroku config:get DATABASE_URL -a your-appIn the new project's Studio, open Database › Import from Postgres, paste the connection string and choose Check source. The check shows the Postgres version, size, schemas, tables and rows, and anything that needs attention: missing extensions, tables without a primary key, roles that will not be created.
Because Heroku has no logical replication, the import does a one-time copy. Stop writes to Heroku first: changes made during the copy are not included.
Choose Start copy. Tables are copied in parallel.
At the end, every table is counted on both sides and small tables are compared row by row with a checksum. The result lists each table as Same rows, Same count or Differs. Sequences are set to the source's values, so new rows do not collide with old ids.
If anything looks wrong, Undo import empties the project again. The Heroku database is never changed by the import.
You can run the same import from the CLI, which is handy for rehearsals:
export STITCHDB_IMPORT_SOURCE="$(heroku config:get DATABASE_URL -a your-app)"
stitchdb import --mode dump
stitchdb import statusThe import docs cover every option.
#Route B: pg_dump and pg_restore
This route works with any destination and gives you a file you can keep.
#1. Take a backup on Heroku
heroku maintenance:on -a your-app # stop writes
heroku ps:scale worker=0 -a your-app # stop background jobs too
heroku pg:backups:capture -a your-app
heroku pg:backups:download -a your-app # saves latest.dumppg:backups:url gives you a temporary link instead, if you want to download the dump straight onto another server. Heroku's backups docs say that link expires after 60 minutes.
Stop worker dynos as well as the web dyno. Maintenance mode blocks HTTP traffic, but a worker or a scheduler job can keep writing.
#2. Restore on the new host
Heroku dumps are in Postgres custom format. Restore without the Heroku owner and grants, since those roles do not exist elsewhere:
pg_restore --no-owner --no-privileges --jobs 4 \
-d "postgresql://p_<ref>_owner:<password>@<db-host>:5432/p_<ref>?sslmode=require" \
latest.dumpOn StitchDB, use the session-mode connection on port 5432 for restores. Transaction mode (6543) is meant for short-lived app connections, not long-running tools. See Connection pooling.
Use a pg_restore version at least as new as the dump's pg_dump. Warnings about extensions or COMMENT ON EXTENSION are common and usually harmless; errors about missing extensions are not.
#3. Re-grant access if you use an API layer
If the new host exposes tables over an API (StitchDB's REST API does), check grants and Row Level Security. Heroku apps rarely use RLS, so the dump brings tables without it. On StitchDB, RLS Guard (on by default for new projects) turns RLS on for every new table in public, so restored tables return no rows through the API until you add policies. On a host without such a guard, turn RLS on yourself:
alter table public.accounts enable row level security;Your server code connects as the owner role, which owns the tables, so owner connections are not filtered by your policies unless you force RLS. If your app only talks to the database from your own server and never through the REST API, this matters less, but it is still a good habit. See Row Level Security.
#Verify before you switch
Do not skip this step, even on a rehearsal that "looked fine".
-- run on both databases and compare
select 'users' as t, count(*) from public.users
union all select 'orders', count(*) from public.orders
union all select 'events', count(*) from public.events;
-- sequences should be at or past the max id
select max(id) from public.orders;
select last_value from public.orders_id_seq;Then run your app's test suite or a smoke test against the new database: sign in, create a record, run a report.
#Switch the app
On Heroku, DATABASE_URL is attached by the Postgres add-on, so you cannot simply overwrite it while the add-on is attached. The usual approach is to have your app read a different variable, or detach the add-on first:
heroku config:set NEW_DATABASE_URL="postgresql://...:6543/...?sslmode=require" -a your-app
# deploy a build that reads NEW_DATABASE_URL, then
heroku ps:scale worker=1 -a your-app
heroku maintenance:off -a your-appWhich port to use depends on your app:
| App | Port on StitchDB |
|---|---|
| Rails / Django / Node web dynos with a connection pool | 5432 or 6543 |
| Many short-lived processes or serverless functions | 6543 |
Migrations, psql, admin scripts | 5432 |
Anything using LISTEN/NOTIFY or session advisory locks | 5432 |
TLS is required on StitchDB: keep sslmode=require in the connection string. A driver that connects without TLS gets the error FATAL: no authentication method is found, which means TLS is missing. If you moved your app off Heroku too, StitchDB can limit database connections to your servers' IP addresses with network restrictions (Pro plan and up).
#Keep Heroku around for a week
Do not delete the Heroku database the same day. Leave it read-only (or just unused) for a week. Download one more backup for your archive. Then remove the add-on.
#Heroku plans versus StitchDB, briefly
As of November 2026, Heroku's add-on page lists Essential plans from $5/month (1 GB storage) to $20/month (32 GB), and Standard-0 at $50/month with 4 GB RAM, 64 GB storage and 200 connections. Heroku's strength is that the database sits next to your dynos with zero setup, and Standard and Premium tiers offer followers and high availability.
On StitchDB, the plan includes the database plus Auth, REST and GraphQL APIs, Storage and Realtime: Pro is $15.99/month with a 10 GB database, 7 days of point-in-time recovery and daily restore points; Scale is $49/month with 20 GB and 14 days. Bigger databases can move to a dedicated server on Scale or Enterprise. The trade-offs: StitchDB is EU-only, so a US-hosted app server will see transatlantic latency on every query, and it is a younger platform than Heroku. Our Heroku Postgres alternatives roundup compares more options.
#FAQ
#Can I migrate Heroku Postgres with zero downtime?
Not with logical replication, because Heroku does not support it for external targets. Plan a write freeze long enough for one full copy and restore, and measure it in a rehearsal.
#Which Postgres version does the destination need?
The same major version or newer. StitchDB's import accepts sources on Postgres 12 or newer.
#Do Heroku Dataclips or followers come along?
No. Followers are Heroku replicas; on the new host, use its own read replicas (StitchDB offers them on paid plans). Recreate Dataclips as saved queries.
#What about Heroku Connect or Kafka connectors?
They are Heroku-specific add-ons and do not move. Replace them before the cut-over.
Ready to rehearse the move? Create a free StitchDB project and import a copy of your Heroku database, or check pricing for larger plans.