Import a Postgres database
Move a Postgres database from another host (Heroku, Render, Railway, Fly, RDS, Supabase, Neon, or your own server) into a new Deplane database. Paste the source's connection URL, check it, and start. For a source that supports it, Deplane keeps the copy in sync while your app still runs on the old database; then you stop writes, press Finish, and switch in under a minute.
Open Databases, then Import a database, or run deplane db import. The command and its flags are in the CLI reference, and every error has a fix in Troubleshooting and error codes.
An import is for people: CI tokens and AI tools cannot start one, and the assistant never takes a database URL or password. The import always creates a new database; it never overwrites one you have.
Two ways to import
Deplane checks the source first and tells you which of the two is possible, with the reasons. The dashboard picks live when it is possible. When the check says copy is possible, it is yours to choose even if live is too.
| Copy | Live | |
|---|---|---|
| How | One consistent snapshot of the source is dumped and loaded into the new database | The same copy, then Deplane keeps following the source's changes until you finish |
| The source needs | Read access to everything | Postgres 10 or newer (13 if it has partitioned tables), wal_level = logical, a role that may replicate, ownership of every table, a primary key or replica identity on every table, no large objects, no unlogged tables |
| Changes on the source | None (shared locks on the tables while it dumps) | One publication and one replication slot (see below) |
| Writes after the start | Not copied | Copied until you finish |
| Downtime | From when you stop writes until the copy is done (minutes per GB) | Writes stop, then Finish: under a minute |
| Checked by | Exact row count of every table, compared with the new database | The same at the end of the copy, again at Finish with the source quiet, and an unchanged schema |
Copy is the simpler choice if your app can be down for a few minutes. Live is for a database that must keep serving.
What Deplane changes on your database
- Copy mode changes nothing. Deplane reads the source in one snapshot. The dump holds shared locks on the tables while it runs, so schema changes (DDL) on the source wait until it ends.
- Live mode adds exactly two things: a publication and a replication slot, both named
deplane_import_followed by the import's ID. Deplane never alters your tables: if a table has no primary key or replica identity, the check says so and names the fix to run yourself (ALTER TABLE ... REPLICA IDENTITY FULL), or you choose copy. - Deplane removes both when the import ends: on finish, on cancel, on failure, and after 14 days of syncing. Until then the slot makes the source keep its change log (WAL), which uses disk on the source. See Before you start.
- Not copied: roles, grants and ownership (everything in the new database is owned by
app, like every Deplane database), other databases on the server, and server-wide objects. Extensions Deplane's Postgres does not have are listed by the check; the ones nothing depends on are left out.
Your source password. The URL is sent once, over HTTPS, in the body of a request: never in an address, a log line or an audit entry. Deplane stores it encrypted for the length of the import and erases it when the import is done, cancelled, failed or expired. In a live import the new database must also hold the password to follow the source: it is stored in the new database (its subscription) until you finish or cancel, and your team can read it there, since it is your own password. It is also in that database's backups for their retention period (7 days on the Beta plan), even after the import ends. If that matters, change the source's password for that role when the import is over, or use a role made for the import and drop it afterwards.
Before you start
Let Deplane connect. If the source only accepts connections from listed addresses, allow the addresses the import page shows ("If your database only accepts listed addresses, allow these"). Deplane connects over TLS by default (sslmode=require); the check warns when the connection is not encrypted. Hosts on private ranges and names that resolve to them are not reachable.
For a live import, on the source:
wal_level = logical(this needs a restart on most servers).- A role with the
REPLICATIONattribute (or your provider's equivalent, such asrds_replication), and ownership of the tables. The publication and the slot belong to the role you imported with. CREATEprivilege on the database, for the publication.- One free replication slot and one free WAL sender (
max_replication_slots,max_wal_senders). - A
pg_hba.confline that lets the role connect to the database from the addresses above. Logical replication connections match the database's own line, so noreplicationline is needed (that keyword is for physical replication only). - Consider
max_slot_wal_keep_size(Postgres 13 or newer). A slot that is not read makes the source keep every change since it was made, which fills its disk. The setting is server-wide: it applies to every slot on the server, including your own replicas and change-data-capture consumers, and a slot that falls behind the limit is invalidated, which breaks it for good. If you set it, choose a value above what your existing replicas and consumers may need to fall behind, and below your free disk. If you are not sure, leave it: Deplane stops the import and removes its slot if the slot holds more than 5 GiB of the source's WAL.
Both modes: the source must be Postgres 9.2 to 17 for copy (10 to 17 for live), and at most 5 GB (see Limits). The Leave out schemas field is optional. Empty, Deplane leaves out the internal schemas of providers it recognises (the report lists them); any list you type replaces those defaults. Anything in the schemas you import that uses a schema you leave out cannot be copied: a foreign key to one of its tables, a row-level security policy or column default that calls one of its functions, a view that reads from it, or a trigger that runs one of its functions. The check refuses both modes and names those objects: include that schema, or drop or rewrite them on the source first.
Finish a live import
A live import goes through Checking, Copying and then Syncing. While syncing, the page shows how far behind the new database is (it should read "Caught up" or a few KB) and when the last change was applied. Nothing changes for your app: it keeps using the old database.
When you are ready to switch:
- Stop writes to the source: scale your app to 0 or turn on maintenance mode.
- Press Finish import (or run
deplane db import finish) and confirm that writes are stopped. - Deplane copies the last changes and the position of every sequence (logical replication does not copy them), compares the row count of every table with the source, checks that the schema did not change, and removes the publication and slot.
- When the import shows Done, attach the new database to your app from its page on Databases. The app gets
DATABASE_URLand rolls out a new release without downtime. Then scale the app back up.
If the schema changed, the new database had not caught up, or a table's count differs, Finish changes nothing and the import goes back to Syncing with the reason, for example that a table has more rows on the source because something is still writing. Fix that and finish again. If the schema changed, cancel and import again after the change: live imports do not copy schema changes, and the page warns as soon as it sees one.
A live import that stays in Syncing for 14 days is cancelled by Deplane (the page warns from day 12). Until the import is done or cancelled, the new database cannot be attached to an app, deleted or restored. Even after a live import ends, the new database cannot be restored to a moment before it ended.
If something goes wrong
- Cancel is available in every state before Done. Copying stops and Deplane removes the publication and slot from the source. The new database stays: a copy, or a live import cancelled before its copy finished, is undone, so the database is left empty; a live import cancelled while it keeps the database in sync leaves the data copied so far. Delete it from Databases if you do not need it. A failed import leaves the new database the same way.
- A failed import ends the same way, with the reason on the page. The error codes list the fixes: import_failed, import_check_failed, import_finish_refused.
- If Deplane cannot reach the source to clean up, the import still ends, and the page shows two SQL lines in a box with a Copy SQL button. Run them on the source as the role you imported with (it needs
REPLICATIONand must own the publication), or as a superuser:
``sql
SELECT pg_drop_replication_slot('deplane_import_<id>');
DROP PUBLICATION IF EXISTS deplane_import_<id>;
``
(the page shows them with your import's ID filled in). Do this soon: until the slot is dropped, the source keeps its change log and its disk keeps filling. (If max_slot_wal_keep_size is set, the source invalidates the slot once it falls too far behind, but you still run the lines to remove it.)
- A check that fails or expires leaves nothing behind. A checked import that you do not start within an hour expires, and its stored password is erased.
Provider notes
Heroku Postgres. Copy only: Heroku does not give you the replication rights live mode needs. Use the app's DATABASE_URL (or the URL from heroku pg:credentials:url). Stop writes (maintenance mode) for the length of the copy, then switch your app. Plan on a few minutes per GB.
Amazon RDS and Aurora. For live, set the parameter rds.logical_replication = 1 in the instance's (or the Aurora cluster's) parameter group and reboot the instance; this sets wal_level = logical. Grant the role rds_replication (GRANT rds_replication TO your_role;) and make sure it owns the tables. RDS leaves max_slot_wal_keep_size at unlimited (-1), so an unread slot can fill the instance's storage. The setting applies to every slot on the instance, so pick a value above what your existing replicas and consumers need, or rely on Deplane's 5 GiB guard, and watch free storage. In the security group, allow the addresses from the import page.
Supabase. The direct database host (db.<project>.supabase.co) is IPv6-only unless you buy the IPv4 add-on, and Deplane connects over IPv4. For a copy, use the session pooler connection string from the project's Connect panel (port 5432 on ...pooler.supabase.com, with the user postgres.<project>). Live replication needs the direct connection, so it only works with the IPv4 add-on. Deplane leaves Supabase's internal schemas (auth, storage, realtime and the like) out by default (not just suggested); your own tables in public are copied. Supabase Auth users and Storage files are not part of this import. Objects that use those schemas: a table such as public.profiles with id REFERENCES auth.users cannot be restored without auth.users, and a policy or default that calls auth.uid() cannot be restored without auth.uid(). The check names every such object it finds (foreign keys, policies, defaults, views and triggers). Either drop or rewrite them on the source before importing (a policy on auth.uid() needs a replacement anyway: Supabase Auth does not come with the import), or type the schemas to leave out yourself, without auth. Deplane then copies auth into the new database as ordinary tables and functions, which can still fail on other Supabase-specific objects: the import then fails with the restore's error.
Neon. For live, turn on logical replication in the project's settings (this restarts the compute) and use the direct endpoint, not the pooled one (the host without -pooler). Copy works with either, but use the direct one anyway.
Render, Railway and Fly Postgres. Use the public connection URL: Render's External Database URL (and add Deplane's addresses under the database's inbound rules), Railway's public network URL (...proxy.rlwy.net), or, for Fly, a public address you have set up for the database. Copy works with any of them once Deplane can connect. Live needs wal_level = logical and a role with REPLICATION (or superuser); where the provider does not let you change that, use copy.
Self-hosted Postgres. Set wal_level = logical and restart, give the import role REPLICATION and CREATE on the database (CREATE ROLE import LOGIN REPLICATION PASSWORD '...'; GRANT CREATE ON DATABASE app TO import;), and make sure it owns the tables (or is a member of the owning role). Add a pg_hba.conf line for each address on the import page, using hostssl. Logical replication connects to the database itself, so the database line is enough (a replication line is for physical replication only):
hostssl app import 203.0.113.10/32 scram-sha-256
Use the addresses the import page shows instead of this example one. Open the port in your firewall for those addresses only, consider max_slot_wal_keep_size (it is server-wide, see above), and remove the role when the import is done.
Limits
| Limit | Value |
|---|---|
| Source size | 5 GB (the database's size as Postgres reports it); larger sources are refused with source_too_large |
| Imports at a time | One active import per team, from the check until it is done, cancelled or failed (import_active) |
| Checks | 20 a day per team (too_many_import_checks) |
| A checked import | Expires after 1 hour if you do not start it |
| The copy step | At most 4 hours |
| Syncing | At most 14 days, then Deplane cancels the import; warned from day 12 |
| Source versions | Postgres 9.2 to 17 for copy, 10 to 17 for live |
| Who can import | People signed in to the dashboard or the CLI; not CI tokens or AI tools |
See also Limits.
This page is also available as Markdown. AI agents can start at /llms.txt.