Skip to content
Browse docs

Migrate an existing database into Layerbase

Bring a database over from another provider without a manual dump and restore. Layerbase reads your source once, copies the schema and data into a managed database, and never modifies the original. You can start from either a provider API key or a raw connection string.

Two ways to start

Seed a new database. Go to Create and choose Migrate to Layerbase (the /cloud/create/from-source flow). Layerbase provisions a fresh database on the matching engine and imports your source into it. This counts as a new database, so if you are at your plan limit you will be asked to free a slot or upgrade first.

Import into an existing database. Open a database you already created and use its Migrate tab. Import into an empty database, and confirm with the checkbox before it runs. A dump-based source (Postgres, MySQL or MariaDB, Redis or Valkey) replaces the target contents, so anything in it is lost. A source read over an API instead (Turso, Cloudflare D1, Algolia) does not clear the target: the copy is added to whatever is there, so a database that already holds data can fail partway or end up with a mix. Use this tab when you created a blank database first (for example to pick a specific name, region, or plan settings).

Supported sources

The source list is grouped by the Layerbase engine you end up on. Migrating a database always keeps you on a compatible engine.

PostgreSQL

Neon, Vercel Postgres, Prisma Postgres, Xata, Supabase, Render, Railway, Replit, Netlify DB, Heroku, DigitalOcean, Fly.io, Aiven, Crunchy Bridge, or any other Postgres by connection string.

MySQL / MariaDB

PlanetScale, DigitalOcean, Aiven, or any MySQL or MariaDB by connection string. A MariaDB source copies into either target, and a MariaDB database you already have on Layerbase can convert to MySQL from its own page. A MySQL 8 source on the default caching_sha2_password auth needs the MySQL target: the MariaDB dump tools cannot read it yet.

Redis / Valkey

Upstash, Vercel KV, Replit (ReplDB), Heroku, DigitalOcean, Aiven, or any Redis or Valkey by connection string. A Redis source is read with a non-blocking scan that copies every key, type, and TTL. A ReplDB store is read over HTTPS from your REPLIT_DB_URL instead, and its keys arrive as plain string values.

DocumentDB or FerretDB (MongoDB-compatible)

MongoDB Atlas or a MongoDB you host yourself, by connection string, into DocumentDB by default or FerretDB if you pick it, and DigitalOcean Managed MongoDB into DocumentDB or FerretDB, picked under Target engine. Collections, documents, and indexes are copied into an engine that speaks the MongoDB wire protocol, so your driver and queries stay the same.

Meilisearch

Algolia. Your index records, searchable attributes, ranking, faceting, and synonyms are translated into Meilisearch.

libSQL

Turso or Cloudflare D1. Turso is read over HTTP with your database URL and an auth token; D1 is read through the Cloudflare API with your account ID and a token. Every SQLite type is preserved.

API key or connection string

Some providers let you paste an account API key. Layerbase uses it once to list the databases in that account so you can pick the one to migrate, then copies it. Neon, Supabase, Render, Railway, PlanetScale, Upstash, Algolia, Turso, and Cloudflare D1 work this way. A few need a second value: Upstash needs your account email, PlanetScale needs the service token ID, Algolia needs its Application ID, and Cloudflare D1 needs your account ID.

Everything else takes a plain connection string. The key is used only to read your source and is never stored. For the least access, create a read-only or project-scoped key where the provider offers one (for example a read-only key in Neon).

Example source connection strings
postgresql://user:password@host:5432/dbname
mysql://user:password@host:3306/dbname
rediss://default:password@host:6379
mongodb+srv://user:password@cluster.example.mongodb.net/dbname

A couple of providers do not expose everything through their API. Supabase never returns the project database password, so you paste it once for the project you choose. PlanetScale is different again: Layerbase mints a short-lived read-only password for the database you pick, copies from it, then deletes it.

Migrating from MongoDB Atlas

A Mongo migration lands on DocumentDB by default, or on FerretDB if you pick it under Target engine before you start, not on MongoDB. Layerbase Cloud does not host the MongoDB server itself, because its Server Side Public License restricts managed offerings. DocumentDB is the MIT-licensed MongoDB-compatible engine originally created by Microsoft, with its own gateway over PostgreSQL storage; FerretDB is the Apache 2.0 implementation of the MongoDB wire protocol on the same DocumentDB extension. Either way your driver, your connection string format, and mongosh keep working. If you need the MongoDB server itself, run it locally with Layerbase Desktop or the Layerbase CLI. Neither engine is the MongoDB server, so check your code against the one you pick before you move. Compare DocumentDB and FerretDB, and for FerretDB, our MongoDB to FerretDB guide has the full table of what it does not implement.

The importer offers the engines your plan can create, and grays out the rest with a link to change plan: if your plan has FerretDB but not DocumentDB, the import lands on FerretDB. To import into a database you already have, open the Migrate tab of an empty DocumentDB or FerretDB database instead.

Paste the mongodb+srv:// string from Atlas with the database name in the path, as in mongodb+srv://user:password@cluster.example.mongodb.net/app. Without it there is no database to copy. The user in the string needs read access to that database, and your Atlas network access list has to allow the connection while the migration runs. The same flow takes a plain mongodb:// string, so a MongoDB you host yourself migrates the same way, as long as we can reach it: the copy runs from our servers, so the source has to be reachable from the public internet. A mongod on a private network, behind a VPN, or on localhost is refused rather than dialed.

The copy reads your cluster while it keeps serving traffic, so it is a live copy rather than a point-in-time snapshot. Documents written partway through can land unevenly across collections, so migrate from a quiet cluster or pause writes for the run. Nothing is written back to Atlas either way, and there is no ongoing sync afterwards: plan a cutover rather than running both for a while. Import into a blank database: a target that already holds documents is refused rather than cleared, so nothing you have can be overwritten by a copy.

Indexes are recreated where the target engine supports them. An index it cannot create is skipped in full, not created with the unsupported options quietly dropped, and every skipped index is named in the migration report at the end, so you can recreate it by hand if your queries need it. On DocumentDB that means an index with a collation, or a hidden index. A capped collection comes across without its cap on either engine. Views and time-series collections are not copied either, and are listed in that report the same way. Read it before you point production at the new database.

Two limits to know before you start: a collection over five million documents is refused, so contact support for a larger migration, and a mongodb:// string listing several hosts is not accepted, so use the mongodb+srv:// form or a single host. Write throughput is the thing to measure yourself: DocumentDB and FerretDB writes both go through PostgreSQL, so a write-heavy workload will not behave exactly as it did on Atlas. Run your own load against the migrated database before you cut over.

Migrating from Cloudflare D1

D1 needs your Cloudflare account ID plus an API token that is account-scoped and carries D1 Read. Read is all Layerbase needs, because the migration only issues read statements against your database. A zone-scoped token cannot work, because Cloudflare has no zone-level D1 permission, and a Global API Key is not a bearer token, so it fails authentication. Create the token under My Profile, then API Tokens.

One thing to plan for: the migration does not take your D1 offline, so it keeps serving traffic while the copy runs, but it is also not a point-in-time snapshot. Layerbase pages through your tables, so rows written partway through the migration can land unevenly across tables. Migrate from a quiet database, or pause writes while the copy runs. Row counts are verified once the copy finishes, so an inconsistent copy is reported rather than silent. The migration form shows this before you paste anything.

When the copy finishes you stay on a completion screen that shows that verification: per-table row counts, anything written to mid-copy, and any follow-up the copy needs (a full-text search table is recreated but its content is not copied, so rebuild it). Read it before you archive or delete the D1 database. The list of your D1 databases shows names without sizes, because Cloudflare only reports a size per database through an extra call for each one, which would eat into your account’s shared D1 API rate limit.

Migrating from Replit

Replit has two kinds of database and they migrate differently. A production database is Neon-backed Postgres with a connection string you can use from anywhere: open your Repl, click Database in the tools pane, go to the Settings tab, and copy the postgresql:// string into the migration. That is the whole flow. A -pooler hostname is fine, because Layerbase switches to the direct endpoint before copying.

A development database (Helium, the Postgres Replit manages itself) is deliberately sealed inside the Repl and is not reachable from the internet, so pasting its DATABASE_URL here cannot work. Move it the other way round instead: create the Layerbase Postgres first, copy its connection string from the database page, then run one command in the Repl shell. Your Layerbase database is the publicly reachable side, so the dump flows out of the sandbox rather than something reaching into it.

Copy a Replit development database from the Repl shell
pg_dump "$DATABASE_URL" --no-owner --no-privileges | psql "<layerbase-connection-string>"

Many Replit apps, and most built with Replit Agent, connect through @neondatabase/serverless. That driver keeps working: Layerbase Postgres implements the same HTTP query protocol, so the neon() client, @vercel/postgres, and Drizzle neon-http accept the Layerbase connection string as-is. The one gap is interactive transactions over WebSocket; if you use those, switch to the standard pg client. Either way the SQL does not change. Then update DATABASE_URL in your Repl secrets and redeploy: the app stays on Replit, only the database moves.

Replit Database (ReplDB), the legacy key-value store behind REPLIT_DB_URL, migrates into a Layerbase Valkey (or Redis, which is wire-compatible). It is a separate store from your Postgres, so it is a separate import: create the Valkey database first, open its Migrate tab, pick Replit, and paste the URL. Find the current one by running echo $REPLIT_DB_URL in the Repl shell.

Find your current REPLIT_DB_URL
echo $REPLIT_DB_URL

Replit rotates that token periodically, so copy it fresh right before you migrate rather than reusing one you saved earlier. Treat it as a credential: anyone holding it can read and write your store. Layerbase uses it for the one copy and never stores it.

Every key in the store is copied across as a plain string value, which is all ReplDB holds: there are no TTLs, hashes, lists, or sets to carry over. A store is capped at 50 MiB and 5,000 keys, so the copy takes seconds. Your ReplDB is read once over HTTPS and never written to.

Migrating from Fly.io

A Fly Managed Postgres cluster is only reachable inside your Fly organization private network, so there is no connection string you can paste that we can dial from here. fly proxy and fly mpg connect forward a port to your own machine, which does not help either: the copy runs from our servers. Push the data out from inside Fly instead. Create the Layerbase Postgres first, copy its connection string, then run one command in a Fly shell. Your Layerbase database is the publicly reachable side, so the dump flows out of the private network rather than something reaching into it.

Copy a Fly Managed Postgres from inside Fly
fly ssh console -a <your-app>

pg_dump "$DATABASE_URL" --no-owner --no-privileges | psql "<layerbase-connection-string>"

Use the direct. connection string rather than the pgbouncer. one, because pg_dump cannot run through a transaction-mode pool.

If you would rather use the guided wizard, give the cluster a public front door first. Fly publishes an MPG proxy app: deploy it with fly launch --from=https://github.com/fly-apps/fly-mpg-proxy --secret CLUSTER_ID=<your-cluster-id>. Three things break this quietly, all confirmed on 2026-09-01. fly launch --from regenerates fly.toml with HTTP defaults, which drops the TCP services the proxy needs, so restore the repository's [[services]] stanza and redeploy before you debug anything else. Allocating a dedicated IPv4 with fly ips allocate-v4 does not remove the shared one the app already had, so release it with fly ips release <shared-ip> or DNS keeps both records and connections fail intermittently. And the string you paste needs ?sslmode=disable, because the direct. endpoint speaks no TLS through the proxy.

The proxied connection string to paste
postgresql://fly-user:<password>@mpg-proxy-something.fly.dev:5432/fly-db?sslmode=disable

Take your direct.<cluster>.flympg.net string, swap in the proxy hostname, and keep port 5432. The proxy ships allowing every address, so restrict its ip-whitelist.txt or delete the app once the copy finishes. Your app can stay on Fly either way: set DATABASE_URL to the Layerbase string with fly secrets set and redeploy.

Auth users come across

If your source database backs an auth system, how the users travel depends on where their credentials live. With Supabase Auth the users and their bcrypt password hashes sit in the database, so they come over intact and nobody has to reset a password. With Neon Auth the passwords live in Stack Auth, not the database, so only profiles migrate and users set a new password on first sign-in. Regular tables migrate normally either way. User ids are copied as they are, so every row that references a user still points at the same one.

Layerbase does not run a Supabase Auth login service, so your app checks the migrated passwords itself. Once the dashboard detects the Supabase Auth schema on the new database, the Connect dialog's snippets include a Supabase sign-in handler that looks a user up in auth.users, verifies the bcrypt hash, and leaves issuing the session to your app.

Row level security policies

Policies are part of your schema, so a Postgres import brings them across: each CREATE POLICY, which tables have row level security switched on, and the auth.uid() style helper functions your policies call. The import report checks all three against your source and names any policy that did not arrive, any table that lost its row level security flag, and any helper function a policy needs that is not there.

Arriving is not the same as applying. The login in your Layerbase connection string is the Postgres superuser, and a superuser bypasses row level security, so a query over that connection sees every row. On a platform that fronts Postgres with its own data API, that API is what switches role and passes the signed-in user's token on every request. Layerbase gives you a plain Postgres connection, so your server does that step itself. Inside a transaction, switch to the role your policies were written for and set the claims they read:

Apply your policies for one request
BEGIN;
SET LOCAL ROLE authenticated;
SET LOCAL request.jwt.claims = '{"sub":"<user id>","role":"authenticated"}';
SET LOCAL request.jwt.claim.sub = '<user id>';
SET LOCAL request.jwt.claim.role = 'authenticated';

SELECT * FROM invoices; -- only the rows this user's policies allow

COMMIT;

Set all three. Which one your policies read depends on when your source project was created: older helper functions read the two single-claim settings, newer ones read the JSON, and setting only the one your helpers ignore returns no rows at all.

Use SET LOCAL, not a plain SET: it ends with the transaction, so the role and claims can never leak to the next request that reuses the connection. Take the user id from a session your own server has verified, never from the client. The roles are created for you without a login, so nothing can connect as them directly, and if your app called the database straight from the browser, those calls need to move behind your server.

Migrating a Redis message queue

Upstash and Vercel KV can back a message queue as well as plain key-value data. A queue can only be migrated into a Valkey database. If you point one of these sources at a Redis target you get the keys and values but not the queue, so create a Valkey database and import there instead. The form tells you this and offers a one-click Valkey create.

What the import reports back

Every import finishes with a report. The screen you started the import from holds on it until you have read it, and if you left the page, the report is on your database’s page for a day after the import finished. If any table on your source did not arrive, the report names it, schema and all, and compares the table count on your source against the table count in your new database. If the restore hit errors, they are listed under the error count so you can see what failed rather than guessing. An import that found anything to review says so in the heading: it never shows a plain "Import complete".

Roles and extensions your dump referenced are handled before the restore runs, so they are not a source of missing tables. Roles the dump expects are created for you, and uuid-ossp is shimmed onto the built-in equivalent. Anything created or shimmed that way is listed in the report, and so is any extension Layerbase does not have, along with the fact that whatever depended on it did not come across. Tables that belong to one of those extensions rather than to you, such as a platform vault or job-queue table, are listed separately and quietly, because none of your data is behind them and counting them as missing would make a complete import read as a broken one. The report also says whether your table and schema privileges came across, so the roles it created for you are not left holding nothing.

A source that uses PostGIS imports too, when the new database runs PostgreSQL 18 or the PostgreSQL 19 beta: the dump’s postgis extension is created and your geometry and geography columns come across with their data. Only core PostGIS ships, so postgis_raster, postgis_topology, postgis_tiger_geocoder, address_standardizer and postgis_sfcgal are not available. If your source used any of them, the report names each one as a PostGIS component Layerbase does not ship, and the objects that belong to it are not copied. See PostGIS for what is included.

pg_cron is handled separately. The dump’s own copy of the cron tables cannot restore, so it is listed with the extension tables rather than as missing. Instead, the import reads your jobs from the source’s cron.job table before the copy, and the screen says how many it found. When the database owner starts the import and the database runs a PostgreSQL version that supports scheduled jobs (see Requirements), the import turns scheduled jobs on (the database restarts once) and recreates each job in your database, paused, so none runs until you review it and resume it. The report lists what was recreated, what was skipped and why (a job that runs in another database on the source, or a name already taken here), and any job that calls something Layerbase does not have, such as pg_net or Vault. It also lists Supabase database webhooks, which are triggers that call pg_net rather than jobs. On other versions, or when a team member started the import, the report lists each job’s definition instead, and once scheduled jobs are on for the database, its owner can create them from the report with Create these jobs (paused). If the source’s job list could not be read, the report says the number of jobs is unknown rather than zero.

What you see while it runs

The import runs on our servers, and the panel shows four named stages: checking your source, copying from your source, loading into your database, and verifying. A stage is either finished, running, or not started yet. There is no percentage and no estimate of time left, and that is deliberate: the only thing we can measure during a copy is the size of the dump file, which is not the same measurement as the size of your source, so a percentage built from the two would be wrong by a different amount for every database.

What you get instead is real. During the copy the panel shows how much has been copied so far, a recent average speed, and how long the import has been running. The loading stage has no size to measure, so it shows only elapsed time and says so. The panel also states the allowance up front, taken from the deadline actually in force for your import: a source we could measure is allowed time sized from its size, a source we cannot measure gets a flat allowance, and every import gets at least the minimum. Small databases finish in seconds. A large one across a slow link can run for hours, and the allowance on the panel is the honest ceiling.

If an import stops making progress, the panel says so while it is still trying, with how long it has been quiet and how long is left before it is stopped automatically. For a source Layerbase reads through its API or a MongoDB connection, that line appears after one minute without progress, and the import is stopped once it has made none for five minutes. For a dump copy, it appears after two minutes without new data, and the copy is stopped once none has arrived for ten. A conversion between two of your own databases checks the row counts after its copy; that check is not stopped this way, so the panel shows no stop for it.

You do not have to keep the page open. Closing the tab does not stop the import, and when you come back, your database’s page shows the import still running, at the stage it has reached. A finished import stays on that page for a day, with its report and how long it took. When it completes, a create-flow migration that came back with nothing to report drops you on the new database, and a Migrate-tab import refreshes the existing database so its size and tabs reflect the imported data. Anything that carries a report holds on the completion screen so you can read it first, then takes you to the database when you are ready. What the report contains depends on the source: missing tables and restore errors for the connection-string sources, row counts per table for the libSQL ones (Turso and Cloudflare D1), and document counts per collection plus any skipped indexes, views, and time-series collections for MongoDB.

If your Neon project had extra branches, Layerbase offers to turn on its own branching after the import rather than replicating them, so you can create instant copy-on-write branches from the migrated database.

If the import fails

When you paste a connection string, Layerbase validates the source before copying: it wakes it, checks it is reachable, and sizes it. A bad password, an unreachable host, or a paused project is caught there with a specific error. An API-key source has no such pre-check: Layerbase opens it server-side when the run starts, so a bad key, a wrong token scope, or a source it cannot read surfaces as a failed import with the error on the panel, beside the stage it stopped in. A failure waits for you on your database’s page too, so closing the tab does not cost you the reason. Either way, in the create flow the new database is provisioned first, so a source that cannot be read leaves you with an empty database to retry from its Migrate tab or delete. Railway sources need public networking enabled on the service, and Algolia needs an Admin key (a search-only key cannot read records or settings).

The failure card says who the failure points at: your source, the connection between your source and Layerbase, a Layerbase limit, or Layerbase itself. When it was a problem on our side, the card says so, we have already been alerted, and your source was not modified: an import only ever reads from it. The full error is still on the card under its details. If the import created the database itself and stopped after copying part of your data, the card offers Delete this database and try again, because an import only runs into an empty database. Nothing is deleted until you confirm it.

Troubleshooting imports

The query console imports files three ways from its Import menu: Import CSV into a table, Run a SQL file, and Restore full database. When one fails, a panel under the Import bar keeps our explanation, the database’s own output, and, when we recognize the failure, what to change before you retry. When the server can tell, the panel also says whether your database was changed: a failure that was rolled back or changed nothing is safe to retry once the file is fixed, and one that applied some statements first needs a look before you run it again. The sections below match the hints on that panel.

A table, type or function does not exist

The file uses an object it never creates. The dump is usually partial or out of order, is a data-only dump going into an empty database, or needs an extension that is not enabled. Re-dump with the full schema: for PostgreSQL, leave out pg_dump --data-only; for MySQL and MariaDB, leave out --no-create-info. To enable an extension first, see Extensions.

An object already exists

Some objects in the file are already in the database. For PostgreSQL, re-dump with pg_dump --clean --if-exists so the file drops each object before creating it. For MySQL and MariaDB, re-dump with --add-drop-table. Or import into an empty database.

Permission denied

A managed database does not let a file change object owners or grant privileges to roles of its own. For PostgreSQL, re-dump with pg_dump --no-owner --no-privileges. For MySQL and MariaDB, remove the DEFINER= clauses on views, triggers, procedures and events: setting a definer needs privileges a managed database does not grant.

The SQL could not be read

The SQL client hit a syntax or compatibility error. This is often a dump made for a different engine, or a PostgreSQL custom-format dump (pg_dump -Fc) uploaded through Run a SQL file, which only takes plain SQL. Restore a custom-format file with Restore full database instead (it replaces everything in the database), or re-export it with pg_dump --format=plain.

The file needs an extension Layerbase does not offer

Before running a PostgreSQL file, Run a SQL file checks its CREATE EXTENSION statements and vector column types. If the file needs an extension Layerbase cannot provide, nothing in it runs and the panel names the extensions. pgvector and pg_net are not offered. Remove the CREATE EXTENSION statements and everything that depends on them (for example vector columns), then upload the file again. See Extensions for what is available.

The file raised its own error

A statement in the file stopped the run on purpose, such as a RAISE EXCEPTION in a PostgreSQL block or a SIGNAL in MySQL or MariaDB. Read its message in the database output, fix the condition it checks, then run the file again.

The file is too large

The file is larger than your plan storage or the import size limit allows. Split it into smaller files and import them one at a time, or free up space or upgrade the plan first.

The import timed out

Each upload is allowed a fixed amount of time to run. A file that needs longer stops partway, so split it into smaller files and import them one at a time.

CSV rows were rejected

The database refused rows in the file, and its output names the row and column. Common causes: when appending to an existing table, a header row that does not match the table’s columns; an empty value in a numeric or date column; and dates not written in ISO form (2026-01-31). Fix the file and import it again.

Sending the failure to support

Every failure panel has a Send diagnostics to support button. It opens a preview of exactly what will be sent: the database ID and engine, which import you used, the file name and size, when it failed, the failure category and request ID when the server reported them, our explanation, and the end of the database’s output. Add a note if it helps, then send. The panel shows your support reference once it is sent. When a failure points at Layerbase rather than your file, the panel says so and puts that button first.