ablo connect), and coordinate every read
and claim through ablo.<model>. Your database stays the system of record: your
app keeps writing through its own backend, Ablo tails your write-ahead log (WAL),
and the confirmed rows fan out to every connected client. Ablo never runs DDL,
owns, or migrates your schema.
No database yet? The hosted sandbox can host rows in Ablo’s test plane —
pass an apiKey only and skip the database setup, like Stripe test mode — so
you can try Ablo before connecting your Postgres.
1. Install and initialize
ablo init scaffolds your project (next step shows what it creates) and ends
by signing you in — one browser click, and a sk_test_ key is saved locally
for the CLI. Later, npx ablo push (step 4) writes ABLO_API_KEY into your
.env.local so the SDK finds it too — no manual copy-paste. npx ablo login
also exists standalone. In CI, or to manage the key by hand, set it yourself
instead:
sk_test_* for the sandbox, sk_live_* for production. In production a key
points at the database you own; in the sandbox you can skip the database
entirely and let Ablo’s test plane host the rows (apiKey only). There is no
keyless mode — a key is always required. (The public /sandbox page is a
separate hosted demo, not your app.)
2. Your Ablo schema (init scaffolded it)
The schema is the contract — it generatesablo.<model> methods for app code,
server actions, agents, and React reads. Declare only the synced models Ablo
coordinates; your auth, billing, and other tables stay in your own Drizzle schema,
owned by your own migrations.
id, createdAt, updatedAt, organizationId, and
createdBy are provided by the SDK automatically. Don’t declare them in your
model(...) fields; declare only your own.
The schema is registered once (init scaffolds ablo/register.ts for you), and
every type is one parameter away — no typeof schema re-stating, anywhere:
.ts module, not a hand-authored .d.ts. The top-level
import type { schema } makes the declare module block merge into (augment)
the SDK’s Register interface instead of colliding with it — the same shape
TanStack Router uses in src/router.tsx. Any .ts file in your
tsconfig include works; it never needs to be imported.
Register binding types every hook and client — it’s the
TanStack-Router pattern: declare the source of truth once, everything
infers from it.)
When you need to name the client type — to pass it to a function or store it in
a context — infer it from the value: type Sync = typeof sync. That’s the
same idiom as tRPC’s typeof appRouter and Drizzle’s typeof db; it resolves
the typed overload at the call site. Avoid ReturnType<typeof Ablo>, which
collapses to the untyped client.
3. Connect your database with ablo connect
Connecting a real database = Postgres logical replication, and ablo connect is
the one way to set it up. Ablo reads your WAL and never runs DDL, owns, or
migrates your schema — your app keeps writing through its own backend.
ablo connect prints the copy-pasteable SQL (a publication named
ablo_publication and a least-privilege ablo_replicator role with
REPLICATION + SELECT, password you choose). ablo connect --check connects to
DATABASE_URL and verifies wal_level=logical, the publication, the role’s
REPLICATION attribute, and that every published table has a usable
REPLICA IDENTITY — a green checklist or the precise fix per item.
REPLICATION role +
the wal_level restart + slot/WAL retention Ablo monitors), and the Preview
status of the WAL runtime are in Connect Your Database.
4. Push the schema, then map it to tables
ablo push uploads the schema definition — model names, fields, types. That
metadata is what tells Ablo which models to coordinate. Skipping it makes every
write to a new or changed model fail with server_execute_unknown_model — that
error literally means “run npx ablo push.”
Now map those models to your real Postgres tables. Your migration tool owns the
tables — Ablo reads them, it does not create or migrate them:
- Run
npx ablo pullto import the shape of your existing tables (created by Prisma, Drizzle, or hand-written migrations) into your schema, ornpx ablo checkto verify your schema and the live tables agree. Keep managing the tables with your own migration tool; Ablo syncs the subset of models you declared and reports the rest as “ignored / owned by you.”
Optional escape hatch: if you have no tables yet and want Ablo to scaffold them,Nothing runs locally — there is no dev server to start. Your app talks to Ablo’s hosted API; the rows live in your database.npx ablo migratecan create your synced-model tables for you. This is not the happy path — connecting a real database isablo connect(step 3), and your own migrations stay in charge of your schema.
5. Write through the model
The rows land in your Postgres; every connected client sees them live.Add coordination for slow work
When AI or background work will touch an existing row for more than a quick write, coordinate throughclaim({ id }). It claims the row and hands a handle
back; claim.state({ id }) reads who is currently working on it without blocking;
and you write the usual way with ablo.<model>.update({ id, data }).
Claims don’t lock. If another writer holds the row, claim waits for them,
re-reads the fresh row, then hands it to you — so two writers serialize instead
of clobbering. Normal reads still work while the claim is held. If a server read
should not return a row while someone else is mid-edit, pass ifClaimed: 'fail'
to error out instead. Reads never block on a claim — to wait for a row to free
up, claim({ id }) it (the claim queues fairly behind the holder).
Call handle.release() when your work is done.
claim waits for them to finish, re-reads, and then hands you the fresh row.
While you hold the claim, update({ id, data }) rejects with AbloStaleContextError
if someone else changed the row first — so you never overwrite work you didn’t
see. Call handle.release() once your work is done.
Multiplayer and claimed work
There is no separate multiplayer mode. Use the same schema client for human UI, server actions, and agents; Ablo fans out confirmed writes and keeps active claims visible on the same model row.claim.state({ id }) tells you when another human or agent is active on the same row.
For schema clients, claim({ id }) waits fairly, re-reads, and then lets you
write through the model.
{ queue: false } on claim when work should be skipped instead of queued
behind an active holder.
Next steps
Keep using the schema client for app and agent writes.- Integration Guide explains the full app, React, Data Source, multiplayer, and agent path.
- Schema Contract explains what the schema drives across SDK, React, agents, Data Source, and schema push.
- Guarantees explains what confirmed writes and stale checks mean.
- Client Behavior covers errors, retries, and public imports.
- Connect Your Database covers the logical-replication connect path end to end —
ablo connect, the honest footprint, and the WAL runtime’s Preview status. - AI SDK Tool shows the same write path inside a tool call.