Connect Your Database
Keep the rows in your own Postgres while Ablo coordinates and confirms every write.
Localhost development
Ablo Cloud cannot dial localhost: from a cloud server, that name means the
cloud server itself, not your Mac or development container. A development child
branch can still use Postgres that listens only on your machine by running a
signed Data Source over Ablo’s outbound reverse channel:
npx ablo migrate # once: models + ablo_idempotency + ablo_outbox
npx ablo dev --local
The command loads ablo/data-source.ts, registers the current child branch as
connector-only, and dials out to Ablo over an authenticated WebSocket. Postgres
continues listening only on your machine; DATABASE_URL never leaves the
process. This is protocol-scoped, not a general-purpose tunnel: only signed
Data Source load, list, commit, and event requests traverse it. Use
--source <path> when the handler lives elsewhere.
Keep ablo dev --local running alongside the application. It pushes schema
changes and owns the database connector; stopping it deliberately makes the
branch’s database unavailable instead of silently writing somewhere else.
Is this full Ablo?
Yes for the Ablo application path: model reads and lists, coordinated writes, claims, subscriptions, idempotency, confirmations, and transactional outbox confirmation all work against localhost Postgres. The browser, server code, and agents still connect to Ablo Cloud; only database operations cross the narrow signed connector to your machine.
It is not logical replication. Visibility depends on how a row is written:
| Write origin | Visible to Ablo in localhost mode? | Why |
|---|---|---|
ablo.<model>.create/update/delete |
Yes | Ablo coordinates the write, the local adapter commits it with idempotency + outbox, and the outbox event confirms it. |
| Code using the signed Data Source adapter | Yes | The adapter records the row and authoritative event in one transaction. |
| A supported source push/outbox integration | Yes | It explicitly publishes the authoritative event to Ablo. |
Raw SQL, psql, or an unrelated ORM write |
No, not automatically | There is no WAL reader in signed-endpoint mode, and bypassing the adapter does not append ablo_outbox. |
If Ablo must observe every arbitrary SQL/ORM write, use the direct logical-WAL path with a network-reachable Postgres endpoint, PrivateLink/peering/VPN, or a database-capable secure tunnel. Do not expose Postgres without TLS, authentication, and network restrictions.
For localhost-first open-source projects
Do not require contributors to buy hosted Postgres or expose port 5432 merely to run the project. Treat the connector as the default contributor topology:
{
"scripts": {
"ablo:setup": "ablo migrate",
"ablo:dev": "ablo dev --local"
}
}
Keep DATABASE_URL=postgres://…@localhost:5432/… in .env.example, commit the
generated ablo/data-source.ts handler, and document two long-running processes:
the application and npm run ablo:dev. Contributors provide their own Ablo
branch credential through ablo login; the repository never contains it.
For collaborative models, route mutations through Ablo or the signed Data Source adapter. If the existing project intentionally writes those same tables through raw SQL or an unrelated ORM path, choose one explicitly:
- add the supported transactional outbox/source-push integration for those writes;
- state that only Ablo-mediated changes participate in live coordination locally; or
- make WAL integration tests opt-in through a secure direct tunnel or hosted test database.
That keeps the zero-cost localhost quickstart honest without weakening Ablo’s coordination boundary or pretending endpoint mode can see WAL.
Local connector errors
Every stable code links to the generated error reference:
| Code | Meaning and fix |
|---|---|
database_loopback_requires_connector |
A direct connection was configured with localhost. For the normal OSS/dev path, run ablo migrate and ablo dev --local; use a direct network route only when arbitrary SQL writes need WAL observation. |
source_connector_not_attached |
The branch is connector-only but no process is attached. Start or restart ablo dev --local. |
source_connector_unauthenticated |
The temporary branch key is missing, expired, or rejected. Rerun ablo dev --local to mint a fresh key. |
source_connector_requires_secret_key |
The connector received the wrong key kind. Let ablo dev supply its branch-bound sk_ key. |
source_connector_no_source_registered |
No endpoint source exists for this branch. Upgrade/rerun the CLI so registration happens before socket attachment. |
source_connector_localhost_required |
Connector-only registration used a non-local descriptor. Use ablo dev --local; deployed handlers use ordinary HTTPS endpoint registration. |
source_connector_timeout |
The handler or local Postgres exceeded the request deadline. Inspect the local process and database. |
source_connector_handler_error |
ablo/data-source.ts or its adapter threw. The local terminal contains the underlying error. |
source_connector_protocol_error |
CLI/SDK and service connector protocols disagree. Upgrade the CLI and SDK together. |
source_connector_production_not_enabled |
A root/production key attempted the development connector. Use a supported production route or explicitly enable production reverse-channel support. |
Disconnects, service restarts, and connector replacement are retryable. Keep the same idempotency key: Ablo never falls back from this branch to hosted storage or another database.
You write through Ablo, and Ablo writes to your Postgres. A call to
ablo.<model>.create / update / delete enters Ablo’s commit chokepoint — where
claims, ordering, and idempotency are enforced — and Ablo applies the change to
your database through a scoped role. Your rows live in your database, which stays
the system of record. Ablo reads your write-ahead log (WAL) to confirm each write
landed and to keep every connected human and agent current.
Ablo writes your rows; it never touches your schema. It runs no DDL and no migrations — your migration tool stays in charge of the shape of your database. Ablo only writes rows into tables you already have, through a role scoped to exactly that.
Just trying Ablo? You don’t need a database to start. Pass an
apiKeyonly, and Ablo keeps your rows in its own log so you can build the whole app today.ablo devgives each Git branch its own isolated plane. Keep it hosted with no database, or point that branch at a separate/local Postgres. Connect your production root (below) when you’re ready for its database to be the system of record.
Connecting sets up two capabilities on your Postgres: logical replication, so
Ablo can read and confirm, and a scoped DML role, so Ablo can write. ablo connect prints the exact SQL. ablo connect apply runs it for you.
Connect commands do not silently load a dotenv file for a mutation. Either
export ABLO_API_KEY and DATABASE_URL, pass --url, or explicitly select the
file:
npx ablo connect apply --env-file .env.local --yes
The explicit flag makes the credential choice visible and loads both the branch-bound key and database URL. Shell environment variables take precedence.
One database, several projects
Provider database URLs and Postgres schemas solve different isolation jobs:
database URL = production, staging, or preview environment
schema = application/project inside that database
ABLO_API_KEY = exact Ablo project branch to bind
It is safe to keep several apps in one production database when each app has its own schema:
ABLO_API_KEY="$MAIL_KEY" DATABASE_URL="$PRODUCTION_URL" \
npx ablo connect apply --schema mail --yes
ABLO_API_KEY="$ENTRIES_KEY" DATABASE_URL="$PRODUCTION_URL" \
npx ablo connect apply --schema entries --yes
For a Neon or Supabase preview branch, use that branch’s direct URL and keep the
schema name stable. Ablo binds one plane to (database, schema): the same
database may add billing, but a second project cannot also claim mail.
Cross-organization conflicts reveal only that the binding is occupied.
Push the Ablo schema before connecting, or pass --tables. The publication is
an explicit list of schema-qualified mapped tables; Ablo never uses a
database-wide FOR ALL TABLES publication for this multi-project path.
If scoped roles already exist but their passwords are unavailable, do not drop
them or run DROP OWNED. Rotate them in place and re-register the fresh
credentials:
npx ablo connect rotate --env-file .env.local --yes
This is the supported recovery after moving a database between branches.
For a one-time release from an older branch, select the named recovery key directly—no shell remapping:
npx ablo connect deregister --key-env OLD_ABLO_KEY --yes
npx ablo connect rotate --env-file .env.local --yes
--key-env reads that exact variable from the process, .env.local, or .env
without printing the secret. Retire the old variable after the move.
Connect in one command
npx ablo connect apply --url postgres://admin:...@host:5432/db --schema mail
Pass an admin connection string with --url and select the application namespace
with --schema (default public). It creates a per-binding publication, two
per-binding scoped roles, and the grants, turns on logical decoding where it can, registers
both scoped roles with Ablo, and proves the setup by reconnecting and reading back.
The admin credential is used on this machine only and never persisted — nothing is
written to your .env, which keeps holding only ABLO_API_KEY. Pass --show-sql
to see every statement first, or drop --apply to print the SQL and run it
yourself. Rotate the scoped passwords any time with ablo connect rotate.
The rest of this page is what that command sets up, step by step, for when you want to run it by hand or review exactly what changes.
Existing rows load automatically
When Ablo creates the replication slot, it takes a consistent initial snapshot of
every mapped table in the publication before following new changes. Rows that
predate ablo connect therefore become available to get, list, and
reactive local.* reads without an application backfill.
Run ablo connect check before removing an existing HTTP/database read fallback.
It reports the initial load as loading until the snapshot is complete. Do not
write a script that updates every row to make it visible: an Ablo update requires
the row to be visible already, and touching application rows is neither necessary
nor a safe bootstrap mechanism.
If a connection was snapshotted with an older replication role whose row-level security hid historical rows, repair that role and request the load again without deregistering or rotating credentials:
npx ablo connect rotate # reasserts BYPASSRLS and safely re-registers both roles
npx ablo connect resnapshot # recreates only the slot; the load is asynchronous
npx ablo connect check # repeat until the existing-row load is complete
Use the same resnapshot step after adding an existing populated table to the
publication. Following its future WAL changes is not enough to load rows written
before publication membership; the snapshot coverage guard therefore refuses to
record completion when even one mapped table is absent. Relation matching is
schema-qualified using the DataSource’s configured schema (default public):
an identically named table in another Postgres schema neither counts as coverage
nor enters the snapshot or WAL stream for your model.
The setup, step by step
1. Enable logical decoding
Turn on logical WAL so Ablo can decode row changes and confirm writes:
ALTER SYSTEM SET wal_level = 'logical';
wal_level is not reloadable — you must restart Postgres for it to take
effect. On Amazon RDS / Aurora you can’t ALTER SYSTEM; set
rds.logical_replication = 1 in the instance’s parameter group instead, then
reboot. (ablo connect apply attempts this for you and, where a managed
provider refuses, hands you the one remaining step.)
2. Run ablo connect for the publication and roles
npx ablo connect
ablo connect prints the exact, copy-pasteable setup SQL for your Postgres.
Run it against your database as a superuser or the DB owner. It creates:
-
A per-binding publication naming only the schema-qualified mapped tables Ablo reads and confirms against. Its suffix is derived from the authenticated Ablo plane and is stable across re-runs:
CREATE PUBLICATION "ablo_publication_<suffix>" FOR TABLE "mail"."messages", "mail"."threads";Override the pushed model set with
--tables a,b,c. -
A replication role: it streams the WAL and
SELECTs, nothing more. This is the role Ablo reads and confirms through. You choose the password; it never passes through Ablo’s CLI or servers:CREATE ROLE "ablo_replicator_<suffix>" WITH NOSUPERUSER BYPASSRLS NOCREATEDB NOCREATEROLE REPLICATION NOINHERIT LOGIN PASSWORD '<password>'; GRANT SELECT ON TABLE "mail"."messages", "mail"."threads" TO "ablo_replicator_<suffix>";BYPASSRLSis required because the initial load is an ordinarySELECT, while logical replication already exposes every row in the publication independently of row-level-security policies. Keep this role’sSELECTgrants scoped to the published tables; it has no write or DDL privileges.On Amazon RDS the
REPLICATIONattribute is granted, not set directly:GRANT rds_replication TO "ablo_replicator";. -
A scoped writer role: the role Ablo writes your rows through. It gets row DML (
SELECT, INSERT, UPDATE, DELETE) and the sync ledger, and nothing else: noREPLICATION, no schemaCREATE,NOSUPERUSER NOBYPASSRLS, row security on. It can change rows in your tables; it cannot change your database:CREATE ROLE "ablo_writer_<suffix>" WITH LOGIN PASSWORD '<write-password>' NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE NOREPLICATION NOINHERIT; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE "mail"."messages", "mail"."threads" TO "ablo_writer_<suffix>";Rename either role with
--role <name>/--write-role <name>.
The schema-local ablo_idempotency ledger lives beside that app’s tables. The
replication slot (ablo_slot_<suffix>) is created and owned by Ablo’s runtime
when it first subscribes — you don’t pre-create it. Registration checks
max_replication_slots first and explains how to free or add capacity.
ablo connect --manual retains the legacy canonical object names for
compatibility and is therefore single-binding within a physical database. Use
connect apply --schema … when several projects share that database.
3. Register the database with Ablo
ablo connect apply already did this. If you ran the SQL by hand instead, hand
Ablo both scoped connection strings once — the replication role it reads and
confirms through, and the writer role it lands your rows through. Set them just
long enough to register:
export ABLO_REPLICATION_DATABASE_URL=... # the replication role
export ABLO_WRITE_DATABASE_URL=... # the writer role
npx ablo connect register
--register posts them to Ablo, which holds them encrypted and uses them to read
and write your database. Ablo holds them from here, so you can drop both from your
environment — your app keeps only ABLO_API_KEY. The role passwords are generated
for you and never printed; rotate them any time with ablo connect rotate. After
this, Ablo does all the connecting.
4. Verify readiness with ablo connect check
npx ablo connect check
--check needs only ABLO_API_KEY. It asks Ablo to check the database it now
holds, from the same infrastructure replication runs on, and prints a green
checklist or the precise per-item fix:
wal_levelislogical- the
ablo_publicationpublication exists - the replication role has the
REPLICATIONattribute - every published table has a usable
REPLICA IDENTITY(a primary key, orREPLICA IDENTITY FULL) soUPDATE/DELETEcan replicate - the writer role is DML-ready — scoped, non-superuser, with the idempotency ledger in place
- the initial snapshot is complete, so rows that existed before connecting are available to Ablo reads
Because Ablo checks from its own network, a database your own machine can’t reach — IPv6-only, IP-allowlisted, behind a VPN — still verifies. Re-run it until every item is green.
Your app holds only the API key, never a connection string:
# .env, server runtime only, never the browser
ABLO_API_KEY=sk_...
import Ablo from '@abloatai/ablo';
import { schema } from './ablo/schema';
export const ablo = Ablo({
schema,
apiKey: process.env.ABLO_API_KEY,
});
The key names its own project and branch, so there is nothing else to configure.
If you want a process to refuse a key you did not expect, pin projectId or
branchId (they default to ABLO_PROJECT_ID and ABLO_BRANCH_ID). Both are
assertions, never routing inputs: during ready() Ablo asks the server what the
key actually targets and refuses to start when a coordinate differs. That is
worth setting where one deployment can be handed keys for more than one
environment, and worth leaving out everywhere else.
The Ablo schema describes only your synced, collaborative models — the rows
Ablo coordinates and fans out in realtime. It is not your whole-database schema
and does not replace your schema.prisma (or Drizzle schema). Your auth,
billing, and other tables stay in your own ORM schema, owned by your own
migrations. ablo check reflects this — it reports tables you didn’t declare as
“ignored / owned by you,” which is exactly right.
5. Write through ablo.<model>
Every change goes through Ablo. The write enters the commit chokepoint, Ablo applies it to your Postgres through the writer role, and the WAL echo confirms it landed:
// Enters the chokepoint (claims, ordering, idempotency), lands in your Postgres.
await ablo.weatherReports.update({ id: 'report_stockholm', data: { high: 21 } });
// Block until your database has it and the WAL echo confirms.
await ablo.weatherReports.update({ id: 'report_stockholm', data: { high: 21 } });
// Reads are live off the same stream.
const report = ablo.weatherReports.local.get('report_stockholm');
A commit is accepted the moment Ablo takes it (queued); it becomes confirmed
once the row appears on your WAL. See Guarantees for what each
state means and when to wait.
What Ablo touches in your database: the honest footprint
This is the complete list. Nothing else.
| Object | What it is | Owned by |
|---|---|---|
ablo_publication |
A publication naming the tables Ablo reads and confirms against. | You create it (step 2). |
ablo_replicator role |
A REPLICATION + SELECT role Ablo reads and confirms through. |
You create it (step 2). |
ablo_writer role |
A scoped DML role Ablo writes your rows through: row DML + ledger, nothing more. | You create it (step 2). |
| Replication slot | A logical slot Ablo subscribes through to track its WAL position. | Ablo’s runtime creates it on first connect. |
wal_level = logical |
A server setting that requires a restart. | You set it (step 1). |
Operational reality you should know up front:
wal_level = logicalneeds a restart. It is a one-time, server-wide change and is not reloadable.- A replication slot retains WAL. While Ablo is connected, the slot holds the WAL it hasn’t yet acknowledged. If Ablo is disconnected for a long time, that WAL accumulates and consumes disk. Ablo monitors slot lag and WAL retention and surfaces it, so disk pressure never surprises you; an abandoned slot is dropped rather than left to grow unbounded.
- The writer role changes rows, not your database. It carries row DML plus the
sync ledger and nothing more — no
REPLICATION, no DDL, no object ownership, and it runs with row security on andNOBYPASSRLS. It is a real, tightly-scoped privilege — describe it that way in a security review.
Ablo runs no DDL and owns no schema: your migration tool stays in charge of the shape of your database, and Ablo writes only rows, only into tables you already have.
What Ablo stores on its side
Your schema definition (model names, fields, types — pushed with ablo push),
your hashed API keys, a safe projection of the connection registration (host,
database, schema — the connection string itself is sealed and never echoed back),
the replication slot position, and the ordered transaction log that drives sync and
coordination. Your rows live in your database.
Postgres replication status: Preview. Registration, readiness checks, and the server replication fleet are implemented and boot-wired. Preview describes product rollout and support, not an inactive code path. Maintainers: see internal/postgres-replication.md for the architecture and operational invariants.
When your database can’t grant replication
Some managed databases won’t grant a REPLICATION role. For those, Ablo connects
through a signed Data Source endpoint instead: you expose one signed HTTP route
built from your ORM (prismaDataSource / drizzleDataSource, with ablo_outbox /
ablo_idempotency bookkeeping), and Ablo writes and confirms through it — same
ablo.<model> surface, same commit chokepoint, same queued → confirmed
lifecycle. It needs no replication setup, which is exactly why it’s the fallback:
reach for it only when logical replication isn’t available, and prefer ablo connect everywhere else.
The current Prisma, Drizzle, and Kysely adapters are PostgreSQL bindings. Their profiles record three independent facts: the database is PostgreSQL, the binding is Prisma/Drizzle/Kysely, and observation is either the transactional outbox or PostgreSQL WAL. An ORM name does not imply that the same adapter supports every database that ORM can connect to.
The outbox automatically observes writes made through Ablo. A write made directly by other application code is visible only if that code writes the same outbox record in its transaction. Native WAL observation sees both Ablo and external writes.
Endpoint events use a versioned envelope. Version 2 freezes syncGroups in the
writing transaction; version 1 is retained only to decode events written by an
older adapter during a rolling upgrade. Poll requests keep cursor (where to
read) separate from acknowledgedThrough (what Ablo has durably accepted), and
the built-in adapters prune acknowledged rows in bounded batches.
Next steps
- Quickstart — connect and write through
ablo.<model>. - Schema Contract — what the schema drives across SDK, React, and agents.
- Guarantees — what confirmed writes and stale checks mean.
- Integration Guide — the full app, React, multiplayer, and agent path.