ablo CLI gets you from an empty project to live-syncing data: scaffold a
schema, connect storage, and verify the model contract. For an existing backend,
the normal path is a signed Data Source endpoint. For the optional direct
Postgres connector, you set DATABASE_URL and ablo migrate provisions just
your synced models plus the adapter’s ablo_outbox / ablo_idempotency
tables — never your whole database. Your defineSchema(...) is the single
source of truth for those synced models; the CLI lowers it to SQL through one
engine (generateProvisionPlan / generateMigrationPlan in
@abloatai/ablo/schema). Everything else — auth, billing, non-synced tables —
stays in your own ORM schema, provisioned by your own migrations.
ablo login, ablo dev, ablo push) authenticate
and sync your Ablo schema to the hosted control plane. Use a
Data Source for your app tables, or ablo migrate when you
opt into the direct Postgres connector.
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
test + live key pair (90-day, restricted) and stores them locally. This
mirrors stripe login.
| Command | What it does |
|---|---|
ablo login | Authorize in the browser; provisions + stores a test and a live key. |
ablo logout | Remove the stored keys. |
ablo status | Show the active org, mode, both keys (prefix + expiry), and server health. |
ablo mode [sandbox|production] | Switch the active mode. With no argument, prompts. |
~/.config/ablo/config.json (mode 0600). In CI, don’t
log in — set ABLO_API_KEY, which always overrides the stored key.
Test vs live
Like Stripe, every account has a test mode and a live mode, and a key belongs to one of them. Test keys are bound to an isolated sandbox: their reads and writes never touch live data. Switch withablo mode; ablo dev is always
test mode by design.
The schema, however, is shared across the org — pushing a schema (from
either mode) defines the same models test and live see; only the rows differ.
Commands
| Command | What it does | Flags |
|---|---|---|
ablo init | Scaffold ablo/ (schema.ts, client, Data Source endpoint, optional agent / component), write .env, install the SDK, and create the app’s project when you’re logged in. Offers to log in at the end. Interactive by default; non-interactive with --yes/no-TTY. | --yes, --framework, --auth, --storage, --project <slug>, --no-project, --no-agent, --no-pull, --no-install, --no-login |
ablo login / logout / status | Authentication & status (above). status shows the active mode AND project. | — |
ablo mode [sandbox|production] | Switch active mode. | — |
ablo projects list|create|use | Manage the org’s projects — each app’s isolated schema/planes/keys. use sets the local active project for new key mints. | create <slug> [--name], use <slug|id|default> |
ablo dev | Hosted — push the schema to your test sandbox, then watch ablo/schema.ts and re-push on save. | --no-watch, --schema <path>, --export <name>, --url <url> |
ablo logs | Tail your scope’s commit activity (stripe logs tail). Follows by default. | -n, --tail <N>, --since <dur|ts>, --model, --op, --json, --no-follow, --mode sandbox|production |
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 pull | Adoption — generate defineSchema(...) from your existing tables (read-only, like prisma db pull). | --out <path>, --app-schema <name>, --import <pkg>, --force |
ablo check | Adoption — 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 |
A schema change is not live until you
ablo push. The sync server imports no
app schema — it learns your models, and which ones are mutable, only from the
copy you push (per tenant). Editing ablo/schema.ts, rebuilding, or redeploying
your app changes nothing on the server by itself. After any schema change —
new model, new field, flipping mutable — run ablo push (or keep ablo dev
running, which re-pushes on save). A write to a model the active pushed schema
hasn’t seen fails with server_execute_unknown_model (see
Structured errors).ablo dev
The development loop. It pushes ablo/schema.ts to your test sandbox,
prints the env line your app needs, then watches the file and re-pushes on every
save (300 ms debounce). It refuses live keys so a tight save loop can never
churn production data.
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.
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.
--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
The table-adoption front door. Instead of migrating (running schema SQL on your database), Ablo
checks the tables you already have: ablo check introspects DATABASE_URL,
compares it 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.
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 vs push
Two front doors to the same engine. Use migrate when you opt into the direct
Postgres connector and want Ablo to provision your synced models in
DATABASE_URL. It only creates the tables for the models in your Ablo schema,
plus the adapter’s ablo_outbox / ablo_idempotency tables — your other tables
are left alone and stay owned by your own migration tool (drizzle-kit, prisma
migrate). push (and dev) run the hosted path: the server registers your
schema version and version-gates connecting clients.
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 |
id, organization_id,
created_by, created_at, updated_at), an organization_id index, and
row-level security keyed on current_setting('app.current_org_id') for tenant
isolation.
.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 samemigration_failed shape on both paths — naming the statement
that broke and the Postgres SQLSTATE, not just “migration failed”.
ablo push (hosted) returns the canonical error envelope (HTTP 500),
which the SDK reconstructs as a typed AbloServerError:
failed and is never activated, so a broken
migration can’t leave clients gated against tables that don’t match.
server_execute_unknown_model
ablo/schema.ts locally but never ran ablo push, or the
model exists but isn’t marked mutable: true. Fix the schema if needed, then
ablo push. (Mutability is opt-in by design: a model is read-only over the wire
unless it explicitly declares mutable: true, so a new model can’t become
client-writable by accident.)
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 |