# Ablo Ablo is collaboration infrastructure for AI agents: one API for agents, apps, and services to claim, change, and confirm the same rows. Here is the problem it solves. Two agents reach for `report_stockholm` at once. One claims the row, does slow work (an LLM call, a fetch, a chain of tools), and commits. The second is neither rejected nor allowed to clobber: it waits in line, is handed the row as it now stands, and proceeds. 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 writers serialize instead of colliding. A person editing that row is simply another holder; the agent waits behind them the same way. And a claim is as narrow as its target: name a `path`, a `range`, or a `field` and two claims on non-overlapping parts of the same row are both granted — region locking within one row, with no queueing between regions. Use AI SDK for the agent loop. Use Ablo when agent reads and writes must persist, coordinate with concurrent work, and leave an audit trail. ## When to use Ablo Reach for Ablo when MORE THAN ONE writer changes the same rows and the second one must not clobber the first. That is the whole judgement. If a single process owns the data, an ORM against your database is simpler and you should use it. These are the jobs it is right for: - **Two or more agents on one workload.** A planner and three workers editing the same records; a swarm assigned overlapping tickets; a retry that restarts while the first attempt is still running. Each takes `claim({ id })` before its slow step, so the second waits and is handed the row as the first left it instead of overwriting. - **A read → LLM call → write gap.** The row can change during the seconds your model is thinking. Pass the `readAt` stamp from your read back into the write and a stale write is refused (`AbloStaleContextError`) rather than silently applied on top of someone else's. - **A human and an agent in the same record.** A person editing in your app is just another holder of the claim. The agent queues behind them; nobody's edit is lost, and neither side needs to know the other exists. - **Work that must survive the process.** Commits are durable and receipts are idempotent, so a crashed or re-scheduled agent replays its write with the same `idempotencyKey` and gets the original result instead of a duplicate. - **Anything you will later have to explain.** Every accepted change lands in an append-only log with who wrote it, when, and against which read. `logs` answers "what did the agents actually do" without you having instrumented for the question in advance. - **Data you are not allowed to move.** Ablo runs on the customer's own Postgres. It tails the replication stream and writes back through a scoped role; your rows never live in Ablo. If a data-residency or procurement rule forbids a vendor holding the records, this is the shape that passes. These are the jobs it is WRONG for, and saying so saves you a wasted integration: - **One writer, no concurrency.** Use Prisma, Drizzle, or SQL. Ablo's coordination costs a round trip and buys you nothing here. - **Chat history, traces, or evals.** Append-only telemetry has no contention to resolve. Write it wherever you write logs. - **A cache, a queue, or a pub/sub bus.** Ablo is a commit chokepoint over a system of record, not a message broker. Emit your events as usual and use Ablo for the state they are about. - **Vector search, embeddings, or retrieval.** Different problem. Ablo coordinates the rows; your vector store indexes them. - **A file, blob, or artifact store.** Coordinate the ROW that points at the object; put the bytes in object storage. How to call it, shortest path first: install `@abloatai/ablo`, run `npx ablo init --yes --framework `, and construct `Ablo({ schema, apiKey: process.env.ABLO_API_KEY })`. Read with `ablo..get({ id })` / `.list({ where })`, write with `ablo..update({ id, data, readAt })`, and wrap anything slow in `await using claim = await ablo..claim({ id })`. The sections below cover the rest; "Start here" is the first thing to run. ## Machine-readable surfaces Everything below is public, needs no credential, and is served from `https://www.abloatai.com`: - `/llms.txt` — this file. - `/llms-full.txt` — the entire published documentation corpus in one fetch. - `/developers` — every developer surface named on one page: the SDK, the API reference, the OpenAPI document, the MCP server, the CLI. - `/openapi.json` — the REST contract as OpenAPI 3.1: every route, a stable `operationId` and description on each, typed responses, and the `ErrorEnvelope` every 4xx and 5xx decodes through. Generate a client from it when no SDK exists for your runtime. - `/mcp` — the integration-helper MCP server over Streamable HTTP. POST your JSON-RPC here; a GET returns a descriptor rather than the protocol. - `/.well-known/mcp.json` — that server's manifest, in the MCP registry's `server.json` format. - `/api/docs/` — any documentation page as plain Markdown, for a client that fetches URLs rather than speaking MCP. - `/sitemap.xml` — every indexable page on the domain. Every page on that host serves a Markdown representation from its own URL: send `Accept: text/markdown` (q-values are honoured), or append `.md` to the path if your client cannot set the header. Responses carry `Vary: Accept`, a client that will accept neither `text/html` nor `text/markdown` gets a `406` listing what is available, and a path that does not exist answers a real `404` — never a `200` carrying a sign-in page. ## Versioning and deprecation Every route lives under `/v1`, and that segment is part of the address you call. A change that would break you arrives as a new segment beside it, never as a change to this one. Additive changes do land in `/v1` — a new response field, a new optional parameter, a new error code — so ignore what you do not recognize. Every response carries `Ablo-Version`, a date stamp for the contract being served. A route being withdrawn says so on itself for at least 180 days first: `Deprecation` (RFC 9745) carries when the deprecation took effect and the route keeps answering; `Sunset` (RFC 8594) carries when it stops. The same operations are marked `deprecated: true` in `/openapi.json`. Responses also carry `RateLimit-Policy` (the standing allowance, e.g. `"secret";q=600;w=12`) and, once your request is attributed to a key, `RateLimit` (what is left and when it refills). A 429 adds `Retry-After` in whole seconds. Pace against these rather than retrying blind. ## Surfaces: pick by who is calling Every surface reaches the same coordinated state. They are not interchangeable. - SDK, `@abloatai/ablo` with `transport: 'http'` — THE AGENTS THEMSELVES. Stateless, request/response, nothing held open; the credential is the identity. This is the main path — default to it. - Coordination MCP, `@abloatai/mcp` — an agent living inside an MCP host (Claude, Cursor). Two jobs: MANAGING your Ablo the way the CLI does (`get_schema`, `list_projects`, `create_project`, `tail_logs`, `get_usage`), and claim/commit as tools over your rows. Call `get_schema` first — it is the only way to learn the model names every data tool needs. `init`, `push`, `pull`, `generate` have no tools (they touch your repo); run those in a shell. - `humans()` with `@abloatai/humans/react` — the interfaces a person watches agent work arrive in. The bare client is the coordination layer (commit, read, observe, claim); `humans()` is the plugin that adds the local watchable copy, live queries, and presence. It `requires: { duplex: true }`, so a `transport: 'http'` agent is rejected at construction rather than left holding a subscription that never delivers. It remains the compatibility default for omitted `plugins`; new code should install it explicitly from `@abloatai/humans`. There is no `agents()` plugin — agents are the default caller, not a special one. A browser NEVER receives the secret key; mint a session token. - CLI, `ablo` — scaffolding, schema push, connecting a database. Terminals and CI. Agents must run it non-interactively (see the CLI section below). - REST, `/api/v1` — runtimes with no SDK. - Integration-helper MCP, hosted `/api/mcp` — teaching a coding assistant the SDK WHILE BUILDING an integration. Docs, schema lint, and scaffolds only; it has NO per-model data tools and cannot touch application rows. The two MCP servers do different jobs and are the most common mix-up: to change application rows use the SDK or the COORDINATION server; the integration-helper server cannot do it at all. Do not hand-roll HTTP against `/api/v1` when a typed SDK exists for the runtime you are in. ## Start here First action when integrating into an app: run `npx ablo init --yes --framework `. Agents have no TTY — `--yes` is REQUIRED or it HANGS. It scaffolds `ablo/schema.ts`, the `Ablo({ schema, apiKey })` client, and (for Next.js) the browser provider + session route. All on the current API. Edit the generated files rather than hand-writing from this doc. Connecting a database is a SEPARATE step with one path — logical replication via `npx ablo connect` (see Storage Boundary); the signed Data Source endpoint is the fallback when database credentials must stay inside the app. Second: make sure a key exists — WITHOUT printing it. The key is a secret; it must never appear in your output, your reasoning, or a file you echo (it would live in the conversation history forever). Check PRESENCE only: `[ -n "$ABLO_API_KEY" ] && echo set` and `grep -cq '^ABLO_API_KEY=' .env.local && echo wired` — never `cat .env.local`, never `echo $ABLO_API_KEY`. If neither check passes, ask the HUMAN to run `npx ablo login` once — it opens a browser and saves a project-scoped branch-management key locally; an agent must NOT run it. You never copy a runtime key by hand: the next step mints one for the development branch, writes it into `.env.local`, and gitignores it. Then PREPARE THE BRANCH AND PUSH — this is the step everything depends on. The server keeps its OWN copy of the schema. Run `npx ablo dev --no-watch --branch `: it ensures an isolated child branch, exchanges the CLI key for an expiring branch-only `sk_` key, writes that key to gitignored `.env.local`, pushes `ablo/schema.ts`, and exits. Until the schema is pushed, EVERY write to a new or changed model fails with `server_execute_unknown_model`. Re-run it after schema changes. Plain `dev` watches forever; never run it bare in an agent. `push` is the lower-level one-shot command for an already selected credential, including a reviewed production deployment. Branches replace manual environment-key juggling. Runtime code always reads ONE `ABLO_API_KEY`; `ablo dev` wires the Git branch's expiring value. Do not create permanent `ABLO_STAGING_KEY` / `ABLO_DEV_KEY` selectors. `npx ablo whoami` strictly confirms the current key's project + immutable branch; `npx ablo whoami --key-env ` explicitly inspects a named process/`.env.local`/`.env` value for CI, migration, or stranded-plane recovery without putting the secret in argv. `status` is the broad readiness report; `whoami` is the narrow identity answer. ## Projects (one org, many apps) Each app gets its own PROJECT inside the org — its own production root + development/preview child branches, each with branch-bound schema, rows, database registration, claims, log, and credentials (the Neon/Supabase shape). `npx ablo init` creates one automatically (slug from package.json name; `--project ` to choose, `--no-project` for the org default). A key belongs to exactly ONE project and branch, fixed at mint; everything it mints inherits that scope. Touching another project's models fails typed: `project_scope_denied` (403) — the fix is a key minted for THAT project, never `ablo push`. Manage: `npx ablo projects list|create |use `; `npx ablo status` shows readiness and `npx ablo whoami` strictly confirms the credential target. Wire: `GET/POST /api/v1/projects` (management authority; duplicate slug → `project_slug_taken` 409). ## Use this API ```ts import Ablo from '@abloatai/ablo'; import { defineSchema, model, z } from '@abloatai/ablo/schema'; TYPES: the project registers its schema ONCE via declaration merging — `npx ablo init` scaffolds `ablo/register.ts` (a regular `.ts` module beside schema.ts, NOT a hand-authored `.d.ts`): `import type { schema } from './schema'; declare module '@abloatai/ablo' { interface Register { Schema: typeof schema } }`. The top-level `import type` makes `declare module` MERGE (augment) the SDK's Register interface rather than collide — same shape TanStack Router uses in src/router.tsx; any `.ts` file in tsconfig include works, never imported. Then model types are one parameter: `type Task = Model<'tasks'>` (import type { Model } from '@abloatai/ablo/schema'); the two-parameter `Model` form is only for projects carrying multiple schemas. Never hand-write model interfaces — derive from the schema. To NAME the client type (function param, context value), infer from the value: `type Sync = typeof sync` — same idiom as tRPC `typeof appRouter` / Drizzle `typeof db`; it resolves the typed overload at the call site. Do NOT use `ReturnType` (collapses to the untyped last overload) and do NOT import a bespoke client-type generic — there is none. const schema = defineSchema({ weatherReports: model({ location: z.string(), status: z.enum(['pending', 'ready']), forecast: z.string().optional(), }), }); const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY }); const report = await ablo.weatherReports.get({ id: 'report_stockholm' }); if (!report) throw new Error('Row not found'); // Claim the row (waits if someone else holds it), read the fresh copy off // `claim.data`, write, then auto-release at the end of this scope (`await using`). await using claim = await ablo.weatherReports.claim({ id: 'report_stockholm' }); const updated = await ablo.weatherReports.update({ id: claim.data.id, data: { status: 'ready', forecast: await getForecast(claim.data) }, }); ``` That is the normal app path: declare models in a schema, then use `ablo..get({ id })`, `ablo..create(...)`, `ablo..update(...)`, and `ablo..delete(...)`. Every verb takes a single options object. Treat the schema as the integration contract. It drives typed model clients, React selectors, server and agent writes, Data Source request/response shape, hosted schema push, and schema-version gating. Do not invent a parallel string-keyed write path for rows that belong to a schema model. For full integrations, use `integration-guide` as the canonical doc. It covers the same model API across your own Data Source-backed app databases, React selectors, multiplayer, and future agent workers. Reads come in two flavors, and you pick by whether you can wait. `get({ id })` (one row) and `list({ where })` (many) are async — they answer from the local graph and fall back to the server, so await them. `local.get(id)`, `local.list({ where })`, and `local.count({ where })` are the same reads narrowed to what has already synced: nothing to await, reactive in render. There is no second verb to learn — `local.` is the only difference. The query reads accept `where`, `filter`, `orderBy`, `limit`, `offset`, and `state`; state defaults to `'live'`, with `'archived'` and `'all'` to include retired rows. `where` takes operators, not only equality: an array value is an `IN` (`{ status: ['draft', 'review'] }`), and tuple form spells the rest out (`[['title', 'ILIKE', '%storm%'], ['createdAt', '>=', cutoff]]`). Clauses combine with AND; for OR, run two reads and union them. `list` returns a page, not always the whole collection. The result is an array, so it maps and iterates as usual, and it carries `hasMore` and `nextCursor` beside the rows. Pass `nextCursor` back as `cursor`, keeping `where` and `orderBy` the same, to walk the rest. Check `hasMore` before treating a result as complete. Workers import the same app schema and select `transport: 'http'`. The transport changes; the typed `ablo.` contract does not. There is no public schema-less or string-keyed model client. React reads should use selector `useAblo`: `useAblo((ablo) => ablo.weatherReports.local.get(id))` (synchronous local read, reactive in render). Use zero-argument `useAblo()` only when a component needs the client for an event handler or effect. The selector form above is the complete reactive surface — every read goes through it. ## Multiplayer Multiplayer is not a separate mode. When human UI, server actions, and agents use the same schema client and write through `ablo.`, Ablo coordinates the shared model stream: confirmed deltas fan out to subscribers, active claims are visible through `claim.state({ id })`, and stale writes can be rejected with `readAt`. If an app writes directly to its own database outside Ablo, that write bypasses coordination until the app reports it through Data Source events. ## Change propagation A change to one row reaches other rows three ways. ROUTING: a write fans out to every sync group the row belongs to, INCLUDING its ancestors' groups (editing a block routes to `block:` + `document:` + `workspace:`), so everyone watching the workspace sees it — delivery, not recomputation. DELETE CASCADE: deleting a parent emits explicit tombstone deltas for its descendants, so open clients never silently hold rows that are gone. VALUE: derived values are NOT recomputed server-side — Ablo surfaces that the source moved and the actor decides. To keep dependent work fresh, declare what you read as a batch premise: `reads: [{ group: 'workspace:abc', readAt: N, onStale: 'notify' }]`. At commit the server checks whether anything in that group moved past `readAt`; `notify` holds the write and returns a `StaleNotification` (re-read the group and regenerate), `reject` aborts. To chain A→B→C, put A+B in one group and B+C in another: A's change reaches B, and C hears it only once B ITSELF writes — the engine wires the edges and signals each hop, the actor walks them. No transitive auto-recompute, no convergence guarantee for cycles. `reads` guards ONE commit; for a long-running actor that reads now and writes much later, register a DURABLE premise with `track`: `ablo..track({ id })` for a row, or the `track:` write option (`track: [{ group: 'workspace:abc' }]`) alongside a write. A track persists server-side; the next time you commit anything, a change that landed on the tracked target rides back on your receipt's `notifications` (same `StaleNotification` shape as `onStale: 'notify'`). It is an idempotent registration, re-baselines so a change fires once, and never notifies you of your own writes. Delivery is on your next commit — a track does not yet push out of band between commits. ## Nouns - `Model client` is the typed `ablo.` object generated from schema. - `Claim` holds a model row while slow work runs; `claim.state({ id })` observes it. - `Commit` is the durable protocol write behind `ablo..update(...)`. - `Receipt` confirms the commit. ## Claimed Behavior Reads never silently block. Schema reads stay open while a row is claimed. Typed reads through `ablo..get({ id })` return the row by default, even while it is claimed. Pass `ifClaimed: 'fail'` to throw `AbloClaimedError`, and inspect active coordination separately through `ablo..claim.state({ id })`. A claim carries app metadata verbatim in `target.meta`. Declare its shape in the SAME register block as the schema — `interface Register { Schema: typeof schema; ClaimMeta: { blocks: string[] } }` — and it types both what you write (`claim({ id, meta })`) and every read (`claim.state`, `claim.queue`, the held claim), so no read needs a `typeof` guard. The per-call `claim.state({ id })` still works and is for a program carrying more than one metadata shape. Reads never block on a claim. To wait for a row to free up, `claim({ id })` it — the claim queues fairly behind the current holder and is granted when the row frees. Use `ifClaimed: 'fail'` when you'd rather refuse to read a claimed row. ## Guarantees Schema model writes update local state optimistically, while their returned promise always waits for authoritative confirmation. Server rejection rolls back local state. To prevent lost updates, read with `snapshot(...)` to capture a `readAt`, then write with `onStale: 'reject'` — the server rejects your update if someone else changed the row after that `readAt`. Claims coordinate writers; they do not block readers. Most users should stay on schema-backed reads/writes and `claim(...)`; manual protocol bookkeeping is not part of the happy path. All SDK errors extend `AbloError`. Important classes: `AbloClaimedError`, `AbloStaleContextError`, `AbloAuthenticationError`, `AbloPermissionError`, `AbloRateLimitError`, `AbloIdempotencyError`, `AbloConnectionError`, `AbloValidationError`, and `AbloServerError`. ## Schema Scope A schema is model fields and relations. Advanced schema helpers such as `mutable`, `readOnly`, `field`, `indexed`, queries, and load strategies exist for offline/cache/indexing-heavy apps; reach for them only after the basic field/relation schema is working. ## Storage Boundary YOU WRITE THROUGH ABLO; ABLO WRITES TO YOUR POSTGRES. In production every schema model is backed by YOUR OWN database. You call `ablo..create/update/delete`; the write enters Ablo's commit chokepoint (claims, ordering, idempotency enforced there) and Ablo applies it to your Postgres through a scoped writer role (`ablo_writer`: `SELECT, INSERT, UPDATE, DELETE`, `NOSUPERUSER NOBYPASSRLS`, `row_security on` — no DDL, no ownership, no schema migration). Ablo then TAILS your write-ahead log (logical replication) to CONFIRM the write landed (`queued` → `confirmed`) and to serve live reads. Your rows live in your database; Ablo holds only the ordered transaction log. Connect in ONE command: `npx ablo connect apply --url ` — it creates the writer + `REPLICATION` roles, the publication, turns on logical decoding, and registers with Ablo; the admin credential is used once and discarded. Your app then holds only `ABLO_API_KEY` — never a connection string. WAL is how Ablo CONFIRMS and READS, NOT the write path — you write THROUGH Ablo. RLS GOVERNS ABLO'S WRITES. The writer role is `NOSUPERUSER NOBYPASSRLS`, and before every write Ablo `SET LOCAL`s a tenant context on the connection (`app.current_org_id`, `app.current_project_id`, `app.current_environment`, participant id/kind) with `row_security = on` — so your row-level security enforces on Ablo's writes. If your policies read a DIFFERENTLY-NAMED GUC (e.g. an `app.current_app_org_id` your app sets per-connection), declare a tenant-context mapping in your schema so Ablo forwards its context into that GUC (ADR 0011) — do NOT carve a policy exception for the writer role. A role that can bypass RLS (e.g. via Neon's `neon_superuser` inheritance) is rejected with `database_role_cannot_enforce_rls`. FALLBACK (only when Ablo can't dial the database, e.g. a private VPC with no inbound route): a signed Data Source endpoint that hands Ablo an ORM `adapter` (Drizzle default; Prisma and Kysely supported) — Ablo writes and confirms through it, and it owns the transaction, exactly-once idempotency, and outbox in ONE pass; your credentials never leave your infrastructure. The constructor is `Ablo({ schema, apiKey })` — the app holds an API key and nothing else; no connection string ever lives in the app. The database is connected out of band with `ablo connect`: writes land in your Postgres through Ablo's server-side commit path using the scoped writer role `ablo connect` creates, and that role is always NON-superuser / NON-BYPASSRLS (see RLS above). ```ts // app/api/ablo/source/route.ts import { dataSourceNext } from '@abloatai/ablo/source/next'; import { drizzleDataSource } from '@abloatai/ablo/source/drizzle'; import { schema } from '@/ablo/schema'; import { db } from '@/db'; export const runtime = 'nodejs'; // the route touches your database export const { POST } = dataSourceNext({ schema, apiKey: process.env.ABLO_API_KEY!, adapter: drizzleDataSource(db, schema), // or prismaDataSource(prisma, schema) / kyselyDataSource(db, schema) }); ``` Connect a database with `npx ablo connect` (logical replication — the one path above). The signed Data Source endpoint (code above) is the fallback when app database credentials must stay private; scaffold it with `npx ablo init --storage endpoint` — Ablo only calls the endpoint. ## Public demo and development branches Public `/sandbox` is a deterministic visual demo. It should teach shared state, claims, stale-write rejection, receipts, and deltas, but it does not use a real API key. It also exposes a Claude Code / Codex handoff prompt. Prefer that shape when an agent is asked to "make Ablo work" in an existing app. Authenticated development branches are real isolated environments. `ablo dev` derives one from Git and mints an expiring `sk_*` key bound to its immutable branch id. A child CAN host rows without your own database, so you can try Ablo with `apiKey` only. (On the production root, your own Postgres is the system of record.) Branch schemas and rows are isolated from siblings and production. There is no shared-sandbox escape hatch and no different production key class: the persisted branch binding distinguishes child from root. For coding agents, the development success path is: pick one shared model, declare schema, prepare an explicit branch with `ablo dev --no-watch --branch `, create the Ablo client, replace one direct mutation with a typed `ablo..update(...)`, use selector `useAblo` for live reads, and add a two-writer stale/claim smoke test. ## Production A PLANE is what a credential acts on: `production` is the root branch and development/preview branches are children. Rows, the registered database, and the active schema artifact are all PER PLANE. Every `sk_` has an immutable persisted branch binding; its spelling does not distinguish root from child, so app code never passes an environment. A child copies its parent's active schema at creation and owns its artifact after that. A CHILD push does NOT reach production. Production gets models only when the reviewed deployment pushes to the root. Going live is three things, each done with a root-bound `sk_` key: register the production database (`ablo connect apply` — the DIRECT host, never a pooler; a pooler refuses in the words of a wrong password), push the schema AHEAD of the code that needs it, and hold the right credential per runtime (server/serverless `sk_`; browser `pk_` read-only or an `authEndpoint` minting `ek_`). `ablo login` stores an `mk_` management credential and CANNOT read/write application data or push the production schema — a production push needs a root-bound `sk_` in `ABLO_API_KEY`. Gate a deploy on `npx ablo status --json` having an EMPTY `blockers` array; each blocker carries a `problem` and the one `fix`. Your agents do NOT each hold a database connection — they talk to Ablo, and Ablo holds at most 4 connections per plane (`application_name = 'ablo-direct-writer'`) however many callers write behind them, so size the database for that number and not for your agent count. Read `deployment` for the full path. ## Public Surface Import from these public paths only: - `@abloatai/ablo` — headless HTTP `Ablo`, errors, typed model clients, claims, durable observation, and `dataSource`. - `@abloatai/ablo/client` — reactive WebSocket client, presence, and the human-facing materializer. - `@abloatai/ablo/schema` — schema DSL. - `@abloatai/ablo/react` — React provider and hooks. - `@abloatai/ablo/source` — `dataSource`, the `DataSourceAdapter` spine, `prismaDataSource`. For a customer-canonical Data Source endpoint. - `@abloatai/ablo/source/next` — `dataSourceNext` (Next.js App Router `{ POST }`). - `@abloatai/ablo/source/drizzle` — `drizzleDataSource`. - `@abloatai/ablo/source/kysely` — `kyselyDataSource`. - `@abloatai/ablo/source/conformance` — `runDataSourceTests` to prove a custom adapter/handler. - `@abloatai/ablo/ai-sdk` — thin AI SDK tools over typed model reads, creates, coordinated updates, and claimed deletes. Do not teach `/api`, `/agent`, `/core`, `/realtime`, or internal subpaths. (`/source` and `/ai-sdk` are public.) ## CLI: agents run it NON-INTERACTIVELY `ablo init` and other prompts need a TTY; an agent/CI run has none and will HANG. Always: - `npx ablo init --yes` (flags: `--framework`, `--auth`, `--storage replication|endpoint`, `--no-agent`, `--no-pull`, `--no-install`, `--no-login`). Generates `ablo/schema.ts` + the `Ablo({ schema, apiKey })` client. `--storage replication` (the default) pairs with `ablo connect`; `--storage endpoint` also scaffolds the `ablo/data-source.ts` fallback endpoint. - `npx ablo connect` connects your database via logical replication — the read path (prints the `wal_level=logical` + publication + `REPLICATION`-role SQL); `npx ablo connect register` registers it, `npx ablo connect check` validates the registered database from Ablo's own side and needs only `ABLO_API_KEY` (no database credential in your environment). `npx ablo connect apply` does the whole setup from a one-time admin connection and leaves your app holding only `ABLO_API_KEY`. - Key: see "Start here" — env → `.env.local` → ask the human to `npx ablo login`; never run `login` yourself, never copy keys by hand (`ablo dev` mints and wires the temporary branch key). - Adopt an existing DB: `npx ablo pull prisma [path]` / `npx ablo pull drizzle `. - `npx ablo dev --no-watch --branch ` ensures an isolated branch, mints and wires its temporary key, pushes once, and exits. Plain `dev` watches forever. `npx ablo branch check ` prints lifecycle, schema/parent compatibility, datasource identity, and readiness. `npx ablo push` pushes with the already selected key and does NOT choose a branch for you. A PRODUCTION push (`sk_` key) needs `--yes`: with no TTY it REFUSES rather than deploying unattended, and interactively it demands the destination project typed by name. Destructive steps (dropped model/field, narrowed enum, lossy cast) need `--force`; a new required field on a populated table needs `--backfill model.field=value`. `npx ablo logs --no-follow` exits instead of tailing forever. `npx ablo push`/`status`/`pull`/`check`/`generate` are one-shot. - `npx ablo docs` lists every documentation page; `npx ablo docs ` prints one as markdown. These pages ship INSIDE the installed package, so they describe the version in `node_modules` and need no network. Prefer them over a docs URL whenever the project pins a version: a website always describes the newest release, so on an older pin it will hand you a call your package does not have (`retrieve`/`list` replaced `get`/`getAll`/`getCount` in 0.35.0). One-shot, safe to run unattended. ## Documentation Canonical docs to read before integrating, in this order. Read each with `npx ablo docs ` when the project pins a version; the links describe the newest release. - [Quickstart](https://docs.abloatai.com/quickstart) - [Branch-first development](https://docs.abloatai.com/branch-development) - [Schema Contract](https://docs.abloatai.com/schema-contract) - [Integration Guide](https://docs.abloatai.com/integration-guide) - [Deployment](https://docs.abloatai.com/deployment) - [Guarantees](https://docs.abloatai.com/guarantees) - [Client Behavior](https://docs.abloatai.com/client-behavior) - [Connect Your Database](https://docs.abloatai.com/data-sources) - [API](https://docs.abloatai.com/api) - Examples: [Existing Python Backend](https://docs.abloatai.com/examples/existing-python-backend), [AI SDK Tool](https://docs.abloatai.com/examples/ai-sdk-tool), [Server Agent](https://docs.abloatai.com/examples/server-agent) - [Upgrade Guide](https://docs.abloatai.com/migration): when upgrading an existing integration; every breaking change, what to change, and which version introduced it. - [Session Settings](https://docs.abloatai.com/session-settings): when the customer's database has row-level-security policies; the identity context Ablo sets before every write, and how to map it to the setting names those policies read. - [Every page, one line each](https://docs.abloatai.com/llms.txt), or [the full docs as one file](https://docs.abloatai.com/llms-full.txt).