deplaneDocs Dashboard

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.

CopyLive
HowOne consistent snapshot of the source is dumped and loaded into the new databaseThe same copy, then Deplane keeps following the source's changes until you finish
The source needsRead access to everythingPostgres 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 sourceNone (shared locks on the tables while it dumps)One publication and one replication slot (see below)
Writes after the startNot copiedCopied until you finish
DowntimeFrom when you stop writes until the copy is done (minutes per GB)Writes stop, then Finish: under a minute
Checked byExact row count of every table, compared with the new databaseThe 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

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:

  1. wal_level = logical (this needs a restart on most servers).
  2. A role with the REPLICATION attribute (or your provider's equivalent, such as rds_replication), and ownership of the tables. The publication and the slot belong to the role you imported with.
  3. CREATE privilege on the database, for the publication.
  4. One free replication slot and one free WAL sender (max_replication_slots, max_wal_senders).
  5. A pg_hba.conf line that lets the role connect to the database from the addresses above. Logical replication connections match the database's own line, so no replication line is needed (that keyword is for physical replication only).
  6. 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:

  1. Stop writes to the source: scale your app to 0 or turn on maintenance mode.
  2. Press Finish import (or run deplane db import finish) and confirm that writes are stopped.
  3. 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.
  4. When the import shows Done, attach the new database to your app from its page on Databases. The app gets DATABASE_URL and 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

``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.)

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

LimitValue
Source size5 GB (the database's size as Postgres reports it); larger sources are refused with source_too_large
Imports at a timeOne active import per team, from the check until it is done, cancelled or failed (import_active)
Checks20 a day per team (too_many_import_checks)
A checked importExpires after 1 hour if you do not start it
The copy stepAt most 4 hours
SyncingAt most 14 days, then Deplane cancels the import; warned from day 12
Source versionsPostgres 9.2 to 17 for copy, 10 to 17 for live
Who can importPeople 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.