Skip to content
AbloAblo Docs
Esc
navigateopen⌘Jpreview
On this page

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 apiKey only, and Ablo keeps your rows in its own log so you can build the whole app today. ablo dev gives 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>";

    BYPASSRLS is required because the initial load is an ordinary SELECT, while logical replication already exposes every row in the publication independently of row-level-security policies. Keep this role’s SELECT grants scoped to the published tables; it has no write or DDL privileges.

    On Amazon RDS the REPLICATION attribute 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: no REPLICATION, no schema CREATE, 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_level is logical
  • the ablo_publication publication exists
  • the replication role has the REPLICATION attribute
  • every published table has a usable REPLICA IDENTITY (a primary key, or REPLICA IDENTITY FULL) so UPDATE/DELETE can 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 = logical needs 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 and NOBYPASSRLS. 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 queuedconfirmed 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.

Was this page helpful?