CLI
Scaffold a schema, connect a database, push it, and watch it sync.
The ablo CLI gets you from an empty project to live-syncing data: scaffold a
schema, authenticate, push the schema, and watch it sync. Your
defineSchema(...) is the single source of truth: whether you run the CLI
locally or push to the hosted server, the same engine turns it into the same
SQL — so what you test is what ships.
npx ablo init # scaffold ablo/schema.ts + client
npx ablo login # authorize in the browser
npx ablo dev # prepare an isolated Git branch + push/watch
Two setup styles, and they pick your commands. If your app database is the
source of truth, expose a Data Source endpoint and keep DB
credentials in your app. If you explicitly want Ablo to open a Postgres
connection, use the Direct Postgres connector commands: ablo migrate
applies changes to your own DATABASE_URL, and ablo check / ablo pull
adopt tables you already have. Hosted branch commands are tagged Hosted;
direct-connector commands are tagged Direct Postgres.
Authenticate
ablo login runs the OAuth 2.0 device flow: it opens your browser, you choose
log in or create an account and approve, and the CLI provisions a
90-day, project-scoped mk_ management credential. It has no test/live mode
and cannot read or write application data. ablo dev uses it to create or
resume a branch and exchanges it for a temporary branch-bound runtime key.
| Command | What it does |
|---|---|
ablo login |
Authorize in the browser; store one project management credential. |
ablo login --project <slug> |
Same, scoped to a project, which becomes active. |
ablo logout |
Remove the stored credentials. |
ablo status |
Show the active org/project, effective credential, branch target, and server health. |
Keys live in ~/.config/ablo/credentials.json (mode 0600), keyed by project.
The non-secret config.json holds the active project. In CI, don’t log in —
set the project management credential as ABLO_MANAGEMENT_KEY; it overrides the
stored credential during branch bootstrap.
Development branches vs live
A branch is your project at full strength over its own rows: the same models, the same schema artifacts, the same claims and the same rules production runs.
Production is the project root. ablo dev creates or reuses a child branch for
your Git branch, then mints a temporary sk_test_ key bound to that child.
Reads, writes, schema artifacts, claims, and credentials stay isolated from
production and from other development branches, which is what makes a
schema-changing pull request as routine as a code-only one.
There is no local mode switch. Development selection comes from Git or
--branch; production authority comes only from an explicit live credential.
Production schema changes use the reviewed one-shot path in
Deployment.
Projects
An org can have multiple projects, each with its own isolated keys, schema,
and data. Keys are scoped to a project at mint and never re-scoped, so the
CLI keeps a separate credential profile per project — Stripe’s
login --project-name model. The active project (set with projects use)
selects which profile every command authenticates with.
| Command | What it does |
|---|---|
ablo projects list |
List the org’s projects (marks the active one and the org-default). |
ablo projects create <slug> |
Create a project (--name "Display Name"). Its keys/schema/data are isolated. |
ablo projects use <slug> |
Switch the active project. ablo projects use default returns to the org-default. |
ablo login --project <slug> |
Store management access for a project and make it active. |
Because keys are fixed to a project, projects use only changes which profile
is active — it never re-scopes an existing key. Switch to a project you haven’t
logged into yet and the CLI tells you to mint one:
npx ablo projects use war-room
# ✓ now targeting project war-room (prj_…)
# No key stored for this project yet — run `ablo login --project war-room` to mint one.
npx ablo login --project war-room # stores its management credential, keeps it active
If you run a project-scoped command (push, dev) while the active project has
no key — but other projects do — the CLI refuses rather than silently
deploying with the wrong project’s credential, and names the fix
(ablo login --project <slug>). In CI, an explicit ABLO_MANAGEMENT_KEY
bypasses profiles for project/branch administration; the runtime key remains
ABLO_API_KEY.
Commands
| Command | What it does | Flags |
|---|---|---|
ablo init |
Scaffold ablo/ (schema.ts, client, optional Data Source / agent / component), write .env, install the SDK. Offers to log in at the end. |
: |
ablo login / logout / status |
Authentication & status (above). | : |
ablo projects list|create|use|rename |
Manage projects and the active one (see Projects). Each project’s keys/schema/data are isolated. | --name "<display>" (create/rename) |
ablo dev |
Hosted: ensure an isolated Git branch, wire its temporary key, push, then watch ablo/schema.ts. |
--branch <slug>, --branch-ttl-hours <1-168>, --no-watch, --schema, --export, --url |
ablo branch list|status|check|create|ensure|credential|delete |
Manage and diagnose immutable branch planes and expiring credentials. | Run ablo branch --help; use --json for automation. |
ablo logs |
Tail the effective credential’s branch activity. Follows by default. | -n, --tail <N>, --since <dur|ts>, --model, --op, --json, --no-follow |
ablo push |
Hosted: upload the schema to Ablo; the server diffs, migrates, and activates it. | --force, --rename old:new, --backfill model.field=value, --schema, --export, --url |
ablo migrate |
Direct Postgres: provision just the synced models (plus the adapter’s ablo_outbox / ablo_idempotency) in your own DATABASE_URL. Leaves your other tables alone. |
--dry-run, --output <file>, --schema, --export |
ablo pull |
Direct Postgres: generate defineSchema(...) from your existing tables (read-only, like prisma db pull). |
--out <path>, --app-schema <name>, --import <pkg>, --force |
ablo check |
Direct Postgres: verify your existing tables fit the schema (read-only, no schema changes). | --schema <path>, --export <name>, --app-schema <name> |
ablo generate |
Emit TypeScript types from the schema. | --out <path>, --schema, --export |
ablo docs |
Read these pages for the version you installed: offline, no network (see ablo docs). |
--json |
ablo docs
The documentation for the version in your node_modules, not the version on the
website.
npx ablo docs # every page, with what it covers
npx ablo docs coordination # one page, as markdown
npx ablo docs --json # the page list, machine-readable
These pages ship inside the npm package, so they describe the code beside them
and stay reachable with no network — the sandboxes and CI runners agents work in
often have none. That matters most when a project is pinned: get / getAll /
getCount became retrieve / list in 0.35.0, and a website always describes
the newest release, so an agent on an earlier version reads the new name and
writes a call its own package doesn’t have.
Pass a slug (coordination), a path (docs/coordination.md), or a file name
(AGENTS.md). A miss names the closest page. The same pages are served over
HTTP at /api/docs/<slug> and through the docs MCP server.
ablo dev
The branch-first development loop. It discovers your Git/CI branch, ensures the
matching Ablo child branch, exchanges the stored mk_ project credential for an
expiring branch-only key, writes that key to gitignored .env.local, pushes
ablo/schema.ts, and re-pushes on every save.
npx ablo dev # discover from Git, push + watch
npx ablo dev --branch preview-pr-482 # explicit branch
npx ablo dev --no-watch # prepare, push once, exit
npx ablo dev --branch-ttl-hours 24 # change temporary-key lifetime
It does not start your app, run migrations, create a database-provider branch, or copy production rows. Read Branch-first development for the exact discovery order, CI flow, database boundary, and troubleshooting.
ablo logs
Tail commit activity, like stripe logs tail. Scope comes from the key — a test
key streams only its sandbox’s writes, a live key the org’s — so you never pass
an org. Follows by default; --no-follow prints recent and exits.
npx ablo logs # last 50, then stream
npx ablo logs -n 100 --model task # backfill 100, one model
npx ablo logs --since 15m --json # last 15m as NDJSON, then stream
Each line is time · op · model · id · actor. --json emits one event per line
(NDJSON) for piping to jq or an agent.
ablo pull
Generate defineSchema(...) from the tables you already have — the inverse of
provisioning, and read-only (like prisma db pull). It introspects
DATABASE_URL, emits a model per adoptable table (one that has id +
organization_id), maps Postgres types back to Zod, and writes ablo/schema.ts.
DATABASE_URL=postgres://… npx ablo pull
It never touches the database, and won’t overwrite an existing schema without
--force. Introspection is lossy — enum members, JSON shape, relations, and
defaults can’t be recovered from columns — so treat the output as a starting
point: review it, then run ablo check.
ablo check
ablo check is how you adopt a database you already own. Instead of creating or
altering tables, it inspects your existing ones and tells you which fit the
schema: it introspects DATABASE_URL, compares each table to your
defineSchema(...), and reports — per model — whether the table is adoptable.
It never writes or alters anything.
A table is adoptable when it has a primary key id and (for org-scoped models)
an organization_id column — the tenancy marker the engine isolates on. Every
other table in your database is ignored.
Why organization_id? It’s the one column that makes a table safe to
multiplayer-sync. Row-level security scopes every read and write by it (org A
can’t see org B’s rows), and the engine routes realtime deltas by org:<id>. A
table without a tenancy key has no isolation boundary, so Ablo excludes it
by default rather than risk exposing it across tenants. If your tenancy
column has a different name, keep that table behind a
Data Source endpoint for now.
DATABASE_URL=postgres://… npx ablo check
✓ tasks → tasks (id, organization_id ok)
✗ projects → projects
• missing "organization_id" — add it, or move this model behind a Data Source
2 models · 1 ok · 1 error
12 other tables in your database — ignored by Ablo
If a table can’t carry organization_id (or has business logic Ablo shouldn’t
bypass), keep it behind a Data Source endpoint rather than
reshaping it. ablo check is read-only; it never proposes a migration.
migrate (Direct Postgres) vs push (Hosted)
Same engine, two setups. If you use the Direct Postgres connector, use
ablo migrate — it provisions the synced models in your own DATABASE_URL. If
Ablo manages the hosted store, use ablo push and ablo dev — the
server applies the change and version-gates connecting clients.
ablo migrate --dry-run # preview the exact SQL
ablo migrate # apply to DATABASE_URL
ablo migrate --output schema.sql # write SQL to a file
One database, two schemas
ablo migrate does not own your whole database. It creates exactly the
models in your defineSchema(...) — the synced, collaborative tables — plus the
adapter’s bookkeeping tables (ablo_outbox, ablo_idempotency). Nothing else.
Your auth, billing, and any other non-synced tables stay in your own ORM
schema (Drizzle’s schema.ts, Prisma’s schema.prisma) and are provisioned by
your own migrations (drizzle-kit push / prisma migrate). The Ablo schema
is not a replacement for schema.prisma, and ablo migrate won’t touch, drop,
or adopt the tables it doesn’t manage. One database, two schemas, side by side —
each owned by its own migration tool.
Zod → Postgres type mapping
The one type map, shared by both paths (there is no second mapping):
| Zod | Postgres |
|---|---|
z.string() |
TEXT |
z.number() |
DOUBLE PRECISION: never INTEGER; a Zod number may be fractional, and truncating is silent data loss |
z.boolean() |
BOOLEAN |
z.date() |
TIMESTAMPTZ |
z.enum([...]) |
TEXT + a CHECK (col IN (...)) constraint |
z.object / z.array / z.record / z.union / z.custom |
JSONB |
.optional() / .nullable() |
nullable column |
Each table also gets the platform columns (id, organization_id,
created_by, created_at, updated_at), an organization_id index, and
row-level security so each org only sees its own rows — the engine sets this per
request (via current_setting('app.current_org_id')); you don’t manage it.
.default(...) is not emitted as a SQL column default — Zod applies the
default at write time (create), in one place, so a DB default and a schema
default can’t drift.
Structured errors
A failed migration aborts the whole transaction (nothing partial lands) and
reports the same migration_failed shape on both paths — naming the statement
that broke and the Postgres SQLSTATE, not just “migration failed”.
ablo migrate (local) logs it:
[migrate] migration plan failed {
code: 'migration_failed',
failedStatement: 'ALTER TABLE "public"."tasks" RENAME COLUMN a TO b;',
failedStatementIndex: 4,
pgCode: '42P01',
durationMs: 133
}
ablo push (hosted) returns the canonical error envelope (HTTP 500),
which the SDK reconstructs as a typed AbloServerError:
{
"type": "AbloServerError",
"code": "migration_failed",
"message": "schema migration failed: relation \"...\" does not exist",
"doc_url": "https://docs.abloatai.com/errors#migration_failed",
"failedStatement": "ALTER TABLE ... RENAME COLUMN a TO b;",
"pgCode": "42P01"
}
The pushed artifact is recorded failed and is never activated, so a broken
migration can’t leave clients gated against tables that don’t match.
Environment
| Variable | Purpose | Default |
|---|---|---|
ABLO_API_KEY |
Authenticate without ablo login (CI). Always overrides the stored key. |
: |
ABLO_API_URL |
Control-plane / API host (push, dev, status). |
https://api.abloatai.com |
ABLO_AUTH_URL |
Dashboard origin for ablo login’s device flow. |
https://abloatai.com |
ABLO_CONFIG_DIR / XDG_CONFIG_HOME |
Where the credential file lives. | ~/.config/ablo |