Read replicas
Send reads to a Supabase read replica and keep read-your-writes after a mutation.
A read replica has
its own API URL. Point SUPABASE_READ_URL at it and the server sends each
request's reads there and its writes to the primary:
SUPABASE_URL=https://abc.supabase.co
SUPABASE_READ_URL=https://abc-rr-eu-west-1-xyz.supabase.coOr pass it in code, where readUrl: false turns an env value off:
import "server-only";
import { createNext } from "better-supabase/next";
import { betterSupabase } from "./index";
export const bs = createNext(betterSupabase, {
readUrl: process.env.REPLICA_URL,
replicas: { pinMs: 5000 },
});Nothing else changes: ctx.db routes each call.
| Call | Goes to |
|---|---|
findMany, findById, count, aggregate, ... | replica |
create, update, upsert, delete | primary |
stable RPCs and read sets (sent as GET) | replica |
| other RPCs | primary |
| anything after a write in the same request | primary |
admin(), dbFor(userId) | primary, always |
Read your own writes
A replica lags the primary by milliseconds to seconds. A page that reads right after saving could miss the row it just wrote. So a successful write pins the request to the primary: every later read in that request goes there too.
The pin also crosses the redirect. When a Next.js action or route, a Hono
or edge handler, or an oRPC procedure served by bs.fetchHandler writes, the
response sets an HttpOnly cookie bs-primary-until that holds a timestamp
replicas.pinMs (5 s by default) ahead. Custom adapters get the same cookie
from ctx.apply(response). Every request with that cookie reads
from the primary until then, so the page you redirect to shows the new data.
Writes through db.$client or ctx.sql bypass the router. Pin by hand after
them:
await ctx.db.$client.rpc("import_customers", { rows });
ctx.replica?.pin();ctx.replica is undefined when no read URL is set.
Other adapters
bs.context(request) reads the cookie for every adapter, so Hono, oRPC
and edge functions honor a pin set by Next.js. Only bs.action and
bs.route set it. Elsewhere, set it yourself when ctx.replica?.wrote is
true:
import { PRIMARY_COOKIE } from "better-supabase/server";
if (ctx.replica?.wrote) {
response.headers.append(
"set-cookie",
`${PRIMARY_COOKIE}=${Date.now() + 5000}; Path=/; Max-Age=5; HttpOnly; SameSite=Lax`,
);
}Caveats
- Realtime, Storage and Auth always use the primary URL.
- A replica rejects writes. A
volatilefunction called withget: truefails there, so mark only read-only functionsstable. - The pin is per browser. Another user can still read stale rows for up to the replica lag.
Last updated on