Skip to main content
The 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.
npx ablo init                 # choose "My database (Data Source endpoint)"
# mount ablo/data-source.ts at /api/ablo/source
Agents & CI — run non-interactively. A coding agent or CI run has no TTY, so two things to know:
  • Prompts hangablo init and bare ablo mode prompt. Use ablo init --yes and ablo mode sandbox|production (with the argument). init also auto-detects no-TTY, but pass --yes to be explicit. Bare ablo mode now errors with the right form instead of hanging.
  • Long-runners never returnablo dev watches and ablo logs follows by default. Use ablo dev --no-watch and ablo logs --no-follow.
npx ablo init --yes --framework nextjs --auth apikey --storage datasource
# also: --no-agent  --no-pull  --no-install  --no-login
Authenticate with the ABLO_API_KEY env var — do not run ablo login (it opens a browser; logging in is a human step that provisions the key the agent then uses). One-shot commands (status, push, pull, check, generate) are safe as-is.
The hosted commands below (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.
CommandWhat it does
ablo loginAuthorize in the browser; provisions + stores a test and a live key.
ablo logoutRemove the stored keys.
ablo statusShow the active org, mode, both keys (prefix + expiry), and server health.
ablo mode [sandbox|production]Switch the active mode. With no argument, prompts.
Keys are stored in ~/.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 with ablo 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

CommandWhat it doesFlags
ablo initScaffold 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 / statusAuthentication & status (above). status shows the active mode AND project.
ablo mode [sandbox|production]Switch active mode.
ablo projects list|create|useManage 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 devHosted — 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 logsTail 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 pushHosted — upload the schema to Ablo; the server diffs, migrates, and activates it.--force, --rename old:new, --backfill model.field=value, --schema, --export, --url
ablo pullAdoption — generate defineSchema(...) from your existing tables (read-only, like prisma db pull).--out <path>, --app-schema <name>, --import <pkg>, --force
ablo checkAdoption — verify your existing tables fit the schema (read-only, no schema changes).--schema <path>, --export <name>, --app-schema <name>
ablo generateEmit 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.
npx ablo dev             # push + watch
npx ablo dev --no-watch  # push once and exit

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

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.
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 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.
ablo migrate --dry-run            # preview the exact SQL
ablo migrate                      # apply synced models to DATABASE_URL
ablo migrate --output schema.sql  # write SQL to a file

Zod → Postgres type mapping

The one type map, shared by both paths (there is no second mapping):
ZodPostgres
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.customJSONB
.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 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 same migration_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:
{
  "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.

server_execute_unknown_model

{
  "type": "AbloValidationError",
  "code": "server_execute_unknown_model",
  "message": "Unknown model: \"presentationSession\". Mark the entity `mutable: true` in the schema to expose it over the wire."
}
This is a write rejected at commit time (HTTP 400), not a migration error. The model is missing from the org’s active pushed schema — almost always because you changed 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

VariablePurposeDefault
ABLO_API_KEYAuthenticate without ablo login (CI). Always overrides the stored key.
ABLO_API_URLControl-plane / API host (push, dev, status).https://api.abloatai.com
ABLO_AUTH_URLDashboard origin for ablo login’s device flow.https://abloatai.com
ABLO_CONFIG_DIR / XDG_CONFIG_HOMEWhere the credential file lives.~/.config/ablo