Migrating from Replit to Layerbase
The app stays on Replit. That is worth saying first, because most people assume a database migration means a platform migration, and this one does not. Your Repl keeps deploying, your secrets panel keeps working, your domain does not move. One environment variable changes and the data lives somewhere you can reach from anywhere.
What makes this move easy is that Replit gave you real Postgres. Production databases are Neon-backed serverless Postgres with a connection string you already have access to, so copying one is a dump and a restore. Development databases are Replit's own Helium Postgres and are deliberately not reachable from outside the Repl, so those get copied from the inside out. Both paths are short. Which one you are on decides everything else in this post.
If you are still deciding whether to move at all, that is a different question and I wrote it up separately in Replit database alternatives, including an honest section on when the built-in database is the right call.
Just want it done? Start at layerbase.com/migrate/replit. You sign in, the wizard opens with Replit already selected, and you paste your Replit Postgres connection string. It provisions a Layerbase Postgres and copies your schema and data in one pass: read-once, nothing written back to Replit. The rest of this post is that same migration explained step by step, plus the development-database path and the manual version.
Contents
- Which database do you have?
- Production: paste the connection string
- Development (Helium): one command from the Repl shell
- The driver question
- Repoint the app: one secret
- Verify the copy landed
- ReplDB to Valkey
- Rehearse it locally first
Which database do you have?
Check this before you do anything else, because the two paths are genuinely different.
| Yours is a... | How you can tell | Path |
|---|---|---|
| Production database | The Database pane shows a connection string under Settings or connection details, and it works from a client on your laptop | Paste the string into the importer |
| Development database | Replit calls it a development database, and the connection string does not work from outside the Repl | Run one command in the Repl shell |
The quick test: copy the connection string, open a terminal on your own machine, and run psql "<string>" -c 'select 1'. If you get a 1, you have a production database. If it hangs or refuses, you have a development (Helium) one, and that is by design rather than a misconfiguration on your side. Replit sandboxes development databases to the Repl, so even a leaked string is useless externally.
Either way, create the destination first. In the Layerbase dashboard, click New database, pick PostgreSQL, and copy its connection string. It looks like this:
postgresql://layerbase:<password>@your-host.cloud.layerbase.dev:5432/app?sslmode=requireThat is a standard, direct Postgres URL over TLS. Keep it somewhere for the next few steps.
Production: paste the connection string
The wizard
On Layerbase Cloud, choose Migrating from another platform and pick Replit (or go straight to layerbase.com/migrate/replit). Paste the connection string from your Repl's Database pane, under Settings or connection details.
Layerbase connects once, copies your schemas, tables, indexes, constraints, sequences, and rows into the new database, and reports what it moved. Nothing is written back to the source, and the connection string is used for the copy rather than stored. For a database under 1GB this is a couple of minutes.
By hand
If you would rather drive it yourself, it is the standard Postgres move:
pg_dump --no-owner --no-privileges "$REPLIT_DATABASE_URL" \
| psql "postgresql://layerbase:<password>@your-host.cloud.layerbase.dev:5432/app?sslmode=require"--no-owner --no-privileges strips ownership and grants that would otherwise fail to restore against a database with different roles. One note carried over from any Neon-backed source: if your connection string has -pooler in the hostname, dump against the direct endpoint instead by removing that suffix. pg_dump opens session-level constructs that a transaction pooler does not support, and the dump either errors or comes out quietly incomplete. The importer handles this rewrite for you; by hand it is on you. The Neon migration guide covers that gotcha in more detail, since it is the same underlying platform.
Development (Helium): one command from the Repl shell
A development database cannot be pulled from outside, but your new Layerbase database is publicly reachable, so you push instead of pull. Open the shell in your Repl and run:
pg_dump "$DATABASE_URL" --no-owner --no-privileges | psql "<layerbase-connection-string>"That is the entire migration. $DATABASE_URL is already set inside the Repl and points at the development database, so you do not have to hunt for credentials. The pipe goes straight out over TLS to your Layerbase database, with no intermediate dump file to clean up (add > dump.sql first if you would rather keep one).
Two things worth knowing:
- If
pg_dumpis not on the Repl's path, install the Postgres client tools in the shell first, or addpostgresqlto your Repl's system dependencies. The version only needs to be recent enough to read your source database. - Run it while nothing is writing. A logical dump of a live database is a point-in-time snapshot per table, not a consistent snapshot of the whole thing, and a development database with an active Agent session is a moving target.
The driver question
This is the step that surprises people, and it has nothing to do with Postgres.
Replit Agent apps very often ship @neondatabase/serverless, which queries over HTTP rather than over a Postgres socket. It is a fine driver. It is also the reason apps broke publicly when Replit moved development databases to Helium: the driver talks to a Neon HTTP proxy, so pointing it at a plain Postgres server does nothing useful. The data was portable the whole time; the client library was not.
You have two options on Layerbase, and neither is a rewrite.
Option 1: keep the driver. Every Layerbase Postgres answers the same HTTP protocol @neondatabase/serverless speaks, so your existing code works against a Layerbase connection string with no package change at all:
import { neon } from '@neondatabase/serverless'
const sql = neon(process.env.DATABASE_URL) // now a Layerbase connection string
const users = await sql`select * from users where id = ${id}`Nothing to configure: the driver derives its endpoint from the hostname in the string. Drizzle's neon-http adapter, @vercel/postgres, and the Prisma and Kysely Neon adapters all sit on the same calls and come along unchanged.
Option 2: swap to pg. Do this if you want a plain TCP driver, or if you need an interactive transaction that spans multiple round trips (BEGIN, application logic, COMMIT), which the HTTP mode cannot do. It is a small edit:
// Before
import { neon } from '@neondatabase/serverless'
const sql = neon(process.env.DATABASE_URL)
const rows = await sql`select id, email from users where id = ${id}`// After
import { Pool } from 'pg'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
const { rows } = await pool.query(
'select id, email from users where id = $1',
[id],
)The shapes differ in two ways worth checking as you go: pg returns a result object, so you read .rows off it rather than getting the array directly, and it takes numbered placeholders instead of a tagged template. If your app has more than a handful of queries, an ORM's Postgres driver (Drizzle's node-postgres, Prisma's default) does the same swap in one config line.
npm install pg
npm uninstall @neondatabase/serverless # only if nothing else uses itRepoint the app: one secret
In your Repl, open Secrets and set DATABASE_URL to the Layerbase connection string. If your code reads a different variable name, change that one. Redeploy.
# Before (Replit-provided)
DATABASE_URL=postgresql://...@ep-cool-name-123456.region.aws.neon.tech/neondb?sslmode=require
# After (Layerbase)
DATABASE_URL=postgresql://layerbase:...@your-host.cloud.layerbase.dev:5432/app?sslmode=requireOne caveat specific to Replit: if the built-in database is still attached to the Repl, Replit may keep injecting its own DATABASE_URL and overwrite yours. If your app connects to the old database after you were sure you changed the secret, that is why. Either detach the built-in database or read a differently named variable (APP_DATABASE_URL is a fine choice) and be explicit in code about which one wins.
Keep the Replit database around for a few days. It costs almost nothing while idle and it is the cheapest rollback you will ever have.
Verify the copy landed
Do this before you consider the migration done, not after someone reports missing rows. Run the same query on both sides and compare:
select schemaname, relname, n_live_tup
from pg_stat_user_tables
order by n_live_tup desc;n_live_tup is an estimate from the statistics collector, which is perfect for spotting a table that came across empty and imprecise for the last few rows. Run analyze; on the new database first to sharpen it, and for any table you actually care about, follow up with an exact count:
select count(*) from orders;Also spot-check the things a dump can carry incorrectly: sequences (insert a row and confirm the new id is not 1), any extensions your schema depends on, and the row your app writes on startup. Then run the app against the new database once before you flip production, which on Replit means setting the secret in a dev run rather than a deployment.
ReplDB to Valkey
If your Repl uses the legacy Replit Database (the key-value store at REPLIT_DB_URL), that one is not Postgres and does not travel with the steps above. It is an HTTP key-value store with hard ceilings: 50 MiB per store, 5,000 keys, 1,000-byte keys, and 5 MiB values.
It has its own importer now. Create a Valkey (or Redis) database on Layerbase, open its Migrate tab, pick Replit, paste REPLIT_DB_URL, tick the acknowledgement, and click Import. Every key comes across as a plain string SET. The store is read once and never written to, and the URL is used for the copy rather than saved anywhere.
Two details decide whether it works on the first attempt:
- The token rotates.
REPLIT_DB_URLis a secret of the formhttps://kv.replit.com/v0/<token>, and Replit reissues it. Runecho $REPLIT_DB_URLin the Repl shell and paste that value straight into the form instead of one you saved last week. - The target has to be empty. The import refuses a database that already holds keys, so point it at one you just created rather than at a cache something is already using.
ReplDB values are plain strings with no TTLs, so the copy is one to one: the same keys with the same bytes, including the awkward ones with spaces or colons in the name. Afterwards any Redis client works with the connection string from the connection panel, and @replit/database's get/set land on GET/SET. The longer version, including what becomes possible once you have real Redis semantics, is in ReplDB to Valkey.
Rehearse it locally first
If this is a database with real users in it, run the whole thing once on your machine before you run it for real. The Layerbase CLI starts a real Postgres locally with no Docker, and it takes the same dump:
npm i -g layerbase
lbase create replit-test -e postgresql --start
psql "$(lbase url replit-test)" < dump.sqlPoint the app at lbase url replit-test, click through the flows that matter, and you will find the driver problem and the sequence problem on your laptop instead of in production.
Wrapping up
The Replit move is smaller than it sounds because Replit did the honest thing and gave you Postgres. Production databases hand you a connection string that any client can use, which makes the copy a paste. Development databases are sandboxed to the Repl, which makes the copy a single command run from inside it. The only genuinely Replit-shaped detail is the driver: if your app ships @neondatabase/serverless, decide deliberately whether to keep it or swap to pg, rather than finding out at deploy time.
After that it is one secret, and the app never leaves Replit.
Keep reading
- ReplDB to Valkey: your Replit key-value store, grown upReplDB is 5,000 keys behind one HTTP endpoint, and it is the smallest database plenty of shipped apps genuinely run on. What its ceilings cost you, how one paste moves the whole store into a managed Valkey, and when staying put is the right answer.
- Replit database alternatives: what you are actually runningThere are three different things called "the Replit database", they behave differently, and only one of them is reachable from outside your Repl. Here is what each one is, why usage billing is an awkward shape for a side project, and when the built-in database is the right call anyway.
- The SSL modes 'prefer', 'require', and 'verify-ca' are treated as aliases for 'verify-full': what this warning meansIf your Node app just started printing a SECURITY WARNING about SSL modes being treated as aliases for verify-full, nothing is broken and nothing has changed yet. Here is what the warning actually means, why it appeared out of nowhere, and the one-line connection string fix.
- From PGlite to Production PostgresPGlite is a real Postgres compiled to WASM, so graduating a prototype to a hosted database is a dump and a restore, not a rewrite. Here is the whole path, start to finish.