sluice

Migrating from a Supabase read replica with sluice

A Supabase read replica (an -rr- endpoint) is a fine bulk-migrate source that offloads the copy's read load from your primary — PG 16+ standby parallel snapshots engage unreduced — but CDC is refused: a replica can't host the sluice publication, so continuous sync must point at the primary. Plus the corrected CDC-preflight facts and how to verify safely against a lagging replica.

A Supabase read replica is managed Postgres running as a streaming-replication standby, so sluice drives it with the vanilla postgres engine. Live-probed 2026-07-17 (PG 17 replica, same region as its primary): it works as a bulk sluice migrate source with the full consistency story, and it is refused as a CDC source with a coded steer to the primary. This guide covers both, plus verifying safely against a replica. For the primary-endpoint essentials (IPv6-only direct host, Supavisor pooler modes, TLS, float display), start with the main Supabase guide.

Bulk migrate: a first-class source #

A read replica is a full bulk-migration source, and the reason to use one is load: it offloads the snapshot's read cost from the production primary. The consistency story is not reduced. pg_export_snapshot() is legal on a PG ≥ 16 standby, so sluice's parallel, snapshot-pinned copy engages unreduced on the replica — one shared snapshot across every table and chunk reader, byte-exact (float8send-proven), evaluated at the replica's replay position.

sluice migrate \
    --source-driver postgres \
    --source 'postgres://postgres.abcdefghijkl-rr-us-east-1-xyz:[email protected]:5432/postgres?sslmode=require' \
    --target-driver postgres --target 'postgres://user:pass@target-host:5432/app?sslmode=require' \
    --dry-run

Reaching the replica: DSN shapes #

A replica has its own hostnames, and the routing differs by whether you use the direct endpoint or the pooler:

PathHow the replica is addressed
Directdb.<ref>-rr-<region>-<suffix>.supabase.co:5432 — its own hostname, IPv6-only exactly like the primary. The IPv4 add-on covers replicas too (one PATCH gives both endpoints A records), but Supabase bills IPv4 per database, so it costs 2× while a replica exists.
PoolerSame host/port as the primary — routing is by username: postgres.<ref> reaches the primary, postgres.<ref>-rr-<region>-<suffix> reaches the replica (session mode :5432 for sluice).

The password is identical to the primary (the role catalog replicates physically). Sanity-check which end you're on with SELECT pg_is_in_recovery()t means you're on the replica.

CDC: point at the primary, never the replica #

CDC has to manage the sluice publication on the source, and CREATE/ALTER PUBLICATION cannot run on a standby. sluice refuses up front with the coded SLUICE-E-CDC-STANDBY-SOURCE steer (before the fix this surfaced as a raw SQLSTATE 25006 “read-only transaction” at publication ensure). PG 16+ standbys can technically host logical slots, but creation blocks on the primary's next running-xacts record and Supabase denies the pg_log_standby_snapshot() nudge that would unblock it — so the primary is the supported CDC source. Point sync start (and backup CDC chains) at db.<ref>.supabase.co, not the -rr- host.

The CDC preflight facts (on the primary) #

Once you're on the primary, three preconditions are worth knowing precisely — the first two are commonly mis-stated:

Verifying against a replica: gate on replay lag #

sluice verify against a lagging replica compares the target with the replica's past — it can false-flag rows the copy correctly took from the primary moments earlier, or false-clean a stale target. Prefer verifying against the endpoint you copied from (self-consistent), and treat the primary as the authoritative sign-off target. If you must verify against a replica, first confirm pg_stat_replication.replay_lag on the primary is ≈ 0 (or that receive-LSN == replay-LSN on the replica). Two operator traps:

What sluice checks for you #

Next steps #