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

Quickstart

Make your first coordinated write, on the Postgres you already have.

Build with Ablo on the Postgres you already have. You declare a small Ablo schema for the models your agents edit together, connect Ablo to your database (ablo connect), and read and write every one of those models through ablo.<model>. You write through Ablo; it lands the change in your Postgres and confirms it by tailing your write-ahead log (WAL). Your rows live in your database, which stays the system of record. Ablo writes rows but runs no DDL and owns no schema — your migration tool stays in charge of the shape of your database.

No database yet? Pass an apiKey only and Ablo keeps your rows in its own log, so you can build the whole app today. Point it at a separate or local Postgres for a development branch, or at your production database when you’re ready.

1. Install and initialize

npm install @abloatai/ablo
npx ablo init

ablo init scaffolds your project (next step shows what it creates) and ends by signing you in — one browser click, and a project-scoped mk_ management credential is saved locally. Later, npx ablo dev (step 4) prepares an isolated branch and writes its temporary ABLO_API_KEY into .env.local, so the SDK follows your Git branch with no manual copy-paste. npx ablo login also exists standalone. In CI, set project management access explicitly:

export ABLO_MANAGEMENT_KEY=mk_...

Every runtime call needs a branch-bound API key. ablo dev exchanges the stored management credential for an expiring sk_* key bound to the current development branch. Production runtimes use sk_*. In production a key points at the database you own; on a development branch you can skip the database entirely and let Ablo 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.)

You do not create a separate named variable per development environment. Application code always reads ABLO_API_KEY; ablo dev wires the value for the current branch. Run npx ablo whoami whenever you want the server-confirmed project and branch for that value. See API Keys for the full project → branch → credential model.

2. Your Ablo schema (init scaffolded it)

The schema is the contract — it generates ablo.<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.

// ablo/schema.ts
import { defineSchema, model, z } from '@abloatai/ablo/schema';

export const schema = defineSchema({
  weatherReports: model({
    location: z.string(),
    status: z.enum(['pending', 'ready']),
    forecast: z.string().optional(),
  }),
});

Reserved fieldsid, 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:

// ablo/register.ts — scaffolded by `npx ablo init`, sits beside ablo/schema.ts
import type { schema } from './schema';
declare module '@abloatai/ablo' {
  interface Register { Schema: typeof schema }
}
export {};

It’s a regular .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.

import type { Model } from '@abloatai/ablo/schema';

type WeatherReport = Model<'weatherReports'>; // fully typed from YOUR schema

The same block is where you name the metadata your claims carry. Add a ClaimMeta key and every claim.state, claim.queue, and held claim reads target.meta as that shape:

declare module '@abloatai/ablo' {
  interface Register {
    Schema: typeof schema;
    ClaimMeta: { blocks: string[] };
  }
}

const holder = ablo.weatherReports.claim.state({ id });
holder?.target.meta?.blocks.length; // typed, no guard

(The same 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

ablo connect sets your database up so Ablo can write your rows (a scoped DML role) and read them back to confirm (logical replication). It writes rows through that role but runs no DDL and owns no schema — your migration tool stays in charge.

You run ablo connect once, out of band — it provisions the roles and hands them to Ablo. From then on Ablo does the connecting; your app never opens a database connection.

# Point it at an admin connection once — it does the whole ceremony: creates the
# roles + publication, turns on logical decoding where it can, registers both
# scoped roles with Ablo, and proves it by reading back. Nothing lands in your .env.
npx ablo connect apply --url postgres://admin:...@host:5432/db

# ...or print the SQL and run it yourself, then register:
#   npx ablo connect            # prints the publication + two scoped roles
#   npx ablo connect check    # validates the database is ready
#   npx ablo connect register # hands the two scoped roles to Ablo

ablo connect apply generates two roles and their passwords — an ablo_replicator role (REPLICATION + SELECT, for reads and confirmation) and an ablo_writer role (scoped row DML, for writes) — and registers both connection strings with Ablo’s control plane, encrypted. Ablo’s runtime uses them to read and write your database. The admin credential you pass to --url is used on this machine only and never persisted. The role passwords are generated for you and never printed — rotate them any time with ablo connect rotate.

Your app holds only the API key — never a connection string:

# .env — server runtime only, never the browser
ABLO_API_KEY=sk_...
// ablo/client.ts
import Ablo from '@abloatai/ablo';
import { schema } from './schema';

export const ablo = Ablo({
  schema,
  apiKey: process.env.ABLO_API_KEY,
});

The full setup, the honest footprint (publication + slot + the REPLICATION and writer roles + the wal_level restart + slot/WAL retention Ablo monitors), and the Preview status are in Connect Your Database.

4. Start the branch development loop

npx ablo dev       # prepare this Git branch, push, and watch ablo/schema.ts

ablo dev discovers your Git branch, ensures a matching isolated Ablo branch, mints an eight-hour branch credential, writes it to gitignored .env.local, uploads the schema definition, and watches for schema edits. Model names, fields, and types tell Ablo which models to coordinate. Skipping the push makes every write to a new or changed model fail with server_execute_unknown_model.

Use npx ablo dev --no-watch when you only need to prepare and push once. Use npx ablo push as the lower-level one-shot command when you deliberately want to push with the currently active key, including a reviewed production deploy. The complete mental model and CI examples are in Branch-first development.

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 pull to import the shape of your existing tables (created by Prisma, Drizzle, or hand-written migrations) into your schema, or npx ablo check to 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.”

Starting from an empty database? npx ablo migrate creates the tables your schema needs. Once they exist, your own migration tool stays in charge of them — Ablo adopts whatever shape you evolve.

No Ablo server runs locally. The ablo dev process only watches your schema; your app talks to Ablo’s hosted API, and the rows live in your database.

5. Write through the model

The rows land in your Postgres; every connected client sees them live.

import { ablo } from './ablo/client';

await ablo.ready();

const created = await ablo.weatherReports.create({
  data: { location: 'Stockholm', status: 'pending' },
});

const updated = await ablo.weatherReports.update({
  id: created.id,
  data: { status: 'ready', forecast: 'Light rain, 13C' },
});

console.log({ id: updated.id, status: updated.status }); // { id: '...', status: 'ready' }

Read a single row back with get({ id }). It resolves to the row, or to undefined when no row has that id — so narrow it once, then the fields are fully typed:

const report = await ablo.weatherReports.get({ id: created.id });
if (!report) throw new Error(`weatherReports ${created.id} not found`);

console.log(report.status); // 'ready'

Add coordination for slow work

When AI or background work will touch an existing row for more than a quick write, coordinate through claim({ 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). Bind the handle with await using and the claim releases itself when the scope exits — on success or on a throw, so a failing agent call never leaves the row locked.

// Claim the row so other participants serialize behind us while we work.
await using handle = await ablo.weatherReports.claim({
  id: 'weather_stockholm',
  description: 'checking_weather',
  ttl: '2m',
});

// Your existing weather tool or agent call. While this runs, other clients
// see that weather_stockholm is being checked.
const weather = await weatherAgent.getWeather(handle.data.location);

await ablo.weatherReports.update({
  id: handle.data.id,
  data: {
    status: 'ready',
    forecast: weather.summary,
  },
});
// scope exit releases the claim — no manual release, even if the work threw

Ablo does not fetch the weather. If another participant already holds the row, 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.

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.

const active = ablo.weatherReports.claim.state({ id: 'weather_stockholm' });
if (active) {
  console.log(`${active.heldBy} is ${active.reason}`);
}

await using handle = await ablo.weatherReports.claim({ id: 'weather_stockholm' });
await ablo.weatherReports.update({ id: handle.data.id, data: { status: 'ready' } });

Use contention: { mode: 'skip' } when work should be skipped instead of queued behind an active holder. Add onStatus inside that object when the attempt should also update telemetry or UI.

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.

Was this page helpful?