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.

FerretDB (MongoDB-compatible)

MongoDB Atlas, DigitalOcean Managed MongoDB, or a MongoDB you host yourself, by connection string. Collections, documents, and indexes are copied into FerretDB, which 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 FerretDB, not on MongoDB. Layerbase Cloud does not host the MongoDB server itself, because its Server Side Public License restricts managed offerings. FerretDB is the Apache 2.0 implementation of the MongoDB wire protocol, storing documents in PostgreSQL through the DocumentDB extensions, so 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. Before you move, check your code against the features FerretDB does not implement, such as $lookup, change streams, $text search, and Atlas Search: our MongoDB to FerretDB guide has the full table.

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 FerretDB 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. 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: FerretDB writes 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.

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.

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.

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