@abloatai/ablo 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/AGENTS.md +3 -3
  2. package/CHANGELOG.md +50 -0
  3. package/README.md +25 -13
  4. package/dist/cli.cjs +1686 -1219
  5. package/dist/coordination/schema.d.ts +4 -2
  6. package/dist/coordination/schema.js +23 -9
  7. package/dist/errorCodes.d.ts +1 -0
  8. package/dist/errorCodes.js +2 -1
  9. package/dist/schema/index.d.ts +1 -1
  10. package/dist/schema/index.js +1 -1
  11. package/dist/schema/schema.d.ts +55 -1
  12. package/dist/schema/schema.js +47 -0
  13. package/dist/schema/select.js +1 -0
  14. package/dist/schema/serialize.d.ts +3 -1
  15. package/dist/schema/serialize.js +9 -1
  16. package/dist/source/adapters/drizzle.js +4 -3
  17. package/dist/source/adapters/kysely.js +4 -4
  18. package/dist/source/adapters/prisma.js +3 -3
  19. package/dist/source/idempotency.d.ts +19 -4
  20. package/dist/source/idempotency.js +19 -4
  21. package/dist/source/migrations.d.ts +4 -1
  22. package/dist/source/migrations.js +36 -5
  23. package/docs/agent-messaging.md +1 -2
  24. package/docs/api.md +4 -4
  25. package/docs/audit.md +35 -23
  26. package/docs/client-behavior.md +7 -4
  27. package/docs/concurrency-convention.md +3 -3
  28. package/docs/coordination.md +13 -1
  29. package/docs/data-sources.md +34 -32
  30. package/docs/examples/agent-human.md +2 -2
  31. package/docs/examples/ai-sdk-tool.md +1 -1
  32. package/docs/examples/nextjs.md +1 -1
  33. package/docs/examples/server-agent.md +2 -2
  34. package/docs/groups.md +160 -0
  35. package/docs/guarantees.md +9 -7
  36. package/docs/how-it-works.md +110 -0
  37. package/docs/integration-guide.md +2 -2
  38. package/docs/interaction-model.md +3 -3
  39. package/docs/quickstart.md +6 -6
  40. package/docs/webhooks.md +23 -22
  41. package/llms.txt +10 -4
  42. package/package.json +1 -1
@@ -446,7 +446,7 @@ scope.
446
446
  ```ts
447
447
  await using claim = await ablo.weatherReports.claim({
448
448
  id: reportId,
449
- reason: 'forecasting',
449
+ description: 'forecasting',
450
450
  });
451
451
  const claimed = claim.data;
452
452
  if (!claimed) return;
@@ -513,7 +513,7 @@ them.
513
513
  | `update({ id, data, ...opts })` | Update through the model client. |
514
514
  | `delete({ id, ...opts })` | Delete through the model client. |
515
515
  | `claim.state({ id })` | See who is currently working on a row (synchronous). |
516
- | `claim({ id, reason?, ttl? })` | Acquire a disposable handle: wait for your turn, re-read, and hold the row. |
516
+ | `claim({ id, description?, ttl? })` | Acquire a disposable handle: wait for your turn, re-read, and hold the row. |
517
517
 
518
518
  Keep first integrations on the model methods above. Every mutation and
519
519
  server-read verb takes one options object; only the synchronous `get(id)` stays
@@ -70,7 +70,7 @@ automatically when the scope exits:
70
70
  ```ts
71
71
  await using claim = await ablo.weatherReports.claim({
72
72
  id: 'report_stockholm',
73
- reason: 'editing',
73
+ description: 'editing',
74
74
  });
75
75
  await ablo.weatherReports.update({ id: claim.data.id, data: { status: 'ready' } }); // rejected if the row changed under the claim
76
76
  ```
@@ -87,8 +87,8 @@ Schema updates can carry `readAt` and `onStale`. If the state advanced past
87
87
  `readAt`, Ablo applies the `onStale` policy:
88
88
 
89
89
  - `reject` — fail the commit (first writer wins).
90
- - `merge` — apply the write if it does not overlap with concurrent changes.
91
- - `force` — apply the write unconditionally.
90
+ - `notify` — accept the write, but flag it for product review.
91
+ - `overwrite` — apply the write unconditionally.
92
92
 
93
93
  The choice is per-commit. No CRDT default; the policy is explicit.
94
94
 
@@ -110,21 +110,21 @@ connection.
110
110
  # Point it at an admin connection once — it does the whole ceremony: creates the
111
111
  # roles + publication, turns on logical decoding where it can, registers both
112
112
  # scoped roles with Ablo, and proves it by reading back. Nothing lands in your .env.
113
- npx ablo connect --apply --url postgres://admin:...@host:5432/db
113
+ npx ablo connect apply --url postgres://admin:...@host:5432/db
114
114
 
115
115
  # ...or print the SQL and run it yourself, then register:
116
116
  # npx ablo connect # prints the publication + two scoped roles
117
- # npx ablo connect --check # validates the database is ready
118
- # npx ablo connect --register # hands the two scoped roles to Ablo
117
+ # npx ablo connect check # validates the database is ready
118
+ # npx ablo connect register # hands the two scoped roles to Ablo
119
119
  ```
120
120
 
121
- `ablo connect --apply` generates two roles and their passwords — an
121
+ `ablo connect apply` generates two roles and their passwords — an
122
122
  `ablo_replicator` role (`REPLICATION` + `SELECT`, for reads and confirmation) and
123
123
  an `ablo_writer` role (scoped row DML, for writes) — and registers both connection
124
124
  strings with Ablo's control plane, encrypted. Ablo's runtime uses them to read and
125
125
  write your database. The admin credential you pass to `--url` is used on this
126
126
  machine only and never persisted. The role passwords are generated for you and
127
- never printed — rotate them any time with `ablo connect --rotate`.
127
+ never printed — rotate them any time with `ablo connect rotate`.
128
128
 
129
129
  Your **app** holds only the API key — never a connection string:
130
130
 
@@ -230,7 +230,7 @@ locked.
230
230
  // Claim the row so other participants serialize behind us while we work.
231
231
  await using handle = await ablo.weatherReports.claim({
232
232
  id: 'weather_stockholm',
233
- reason: 'checking_weather',
233
+ description: 'checking_weather',
234
234
  ttl: '2m',
235
235
  });
236
236
 
package/docs/webhooks.md CHANGED
@@ -1,30 +1,22 @@
1
1
  # Webhooks
2
2
 
3
- Ablo owns the ordered transaction log the source of truth. **Webhooks are how
4
- that log reaches your database.** Ablo streams every committed change to an
5
- endpoint in your app as a signed event; your handler writes your own database.
3
+ Ablo keeps an ordered transaction log of every committed change and coordinates
4
+ the writers people and agents that produce it. Your rows live in your own
5
+ database; Ablo holds only the log. **Webhooks stream that log to your systems as
6
+ signed events:** every committed change is POSTed to an endpoint in your app, and
7
+ your handler decides what to do with it.
6
8
 
7
9
  It's the same two-sided shape as Stripe: you call Ablo to make changes (the
8
- client), and Ablo calls you to persist them (this webhook). Ablo never holds your
9
- database credentials.
10
+ client), and Ablo calls you with each change (this webhook). Webhooks are the
11
+ *push* way to keep a store in step with the log — your own database, a warehouse,
12
+ a search index, a background job. The *direct* alternative is `ablo connect`,
13
+ where Ablo reads your write-ahead log and writes back through a scoped role. Either
14
+ way your handler owns the write: the webhook path gives Ablo no database
15
+ credentials at all.
10
16
 
11
17
  ## The loop
12
18
 
13
- ```mermaid
14
- flowchart LR
15
- App["Your app<br/>(the client)"]
16
- subgraph Ablo["Ablo (hosted)"]
17
- Log["Transaction log<br/><i>source of truth</i>"]
18
- end
19
- Clients["Other clients<br/>(live, optimistic)"]
20
- Route["/api/ablo/[...all]<br/>(your webhook route)"]
21
- DB[("Your database")]
22
-
23
- App -->|write| Log
24
- Log -->|realtime sync| Clients
25
- Log -->|signed event| Route
26
- Route -->|write| DB
27
- ```
19
+ ![Your app writes to Ablo's ordered log — the source of truth, which holds the log and coordination only, never your rows. The log fans out two ways: realtime sync to live clients for an instant, optimistic UI, and a signed event to your webhook route, which writes the durable copy into your own database.](/the-loop.svg)
28
20
 
29
21
  There are two ways data flows out of Ablo, and they're for different jobs:
30
22
 
@@ -55,8 +47,8 @@ Every delivery is a batch of events. Each event:
55
47
 
56
48
  | field | meaning |
57
49
  |---|---|
58
- | `type` | `"<model>.<verb>"`, e.g. `task.updated` |
59
- | `model` | the model name (`task`) — the table to write |
50
+ | `type` | `"<model>.<verb>"` with the model name lowercased, e.g. `task.updated` |
51
+ | `model` | the model name exactly as declared in your schema — the table to write |
60
52
  | `objectId` | the changed row's id |
61
53
  | `data` | the post-change row, or `null` on delete (like Stripe's `event.data.object`) |
62
54
  | `syncId` | monotonic log position — **dedupe and order by this** |
@@ -138,12 +130,21 @@ npx ablo webhooks create https://yourapp.com/api/ablo/[...all]
138
130
  # ✓ Wrote ABLO_WEBHOOK_SECRET to .env.local (shown once)
139
131
  ```
140
132
 
133
+ Scope which models fire and label the endpoint at creation with `--events` and
134
+ `--description` (both optional; events default to `*` = every model):
135
+
136
+ ```bash
137
+ npx ablo webhooks create https://yourapp.com/api/ablo/[...all] \
138
+ --events task,project --description "prod mirror"
139
+ ```
140
+
141
141
  Manage and inspect endpoints:
142
142
 
143
143
  ```bash
144
144
  npx ablo webhooks list # endpoints + delivery health (status, cursor, last error)
145
145
  npx ablo webhooks roll <id> # mint a fresh signing secret
146
146
  npx ablo webhooks enable <id> # re-enable a disabled endpoint
147
+ npx ablo webhooks rm <id> # remove an endpoint
147
148
  ```
148
149
 
149
150
  Or call the API directly — the org is derived from your secret key:
package/llms.txt CHANGED
@@ -88,6 +88,10 @@ visible through `claim.state({ id })`, and stale writes can be rejected with `re
88
88
  If an app writes directly to its own database outside Ablo, that write bypasses
89
89
  coordination until the app reports it through Data Source events.
90
90
 
91
+ ## Change propagation
92
+
93
+ 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 layer routes to `layer:` + `slide:` + `deck:`), so everyone watching the deck 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: 'deck: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.
94
+
91
95
  ## Nouns
92
96
 
93
97
  - `Model client` is the typed `ablo.<model>` object generated from schema.
@@ -123,11 +127,13 @@ A schema is model fields and relations. Advanced schema helpers such as `mutable
123
127
 
124
128
  ## Storage Boundary
125
129
 
126
- THE ONE PATH IS LOGICAL REPLICATION. In production every schema model is backed by YOUR OWN database, and Ablo connects to it by CONSUMING ITS WRITE-AHEAD LOG (logical replication) the Electric/PowerSync/Zero model. Ablo tails the WAL and fans changes out; YOUR APPLICATION CONTINUES TO OWN THE WRITE PATH (your own backend, your existing API). Ablo never runs DDL on, writes to, owns, or migrates your database. Connect once with `npx ablo connect` (prints the setup SQL: `wal_level=logical`, a publication, a `REPLICATION` role) then `npx ablo connect --register`. Validate with `npx ablo connect --check`. The role's privilege footprint is narrow but real a `REPLICATION`-attributed role that streams the WAL and `SELECT`s; do NOT describe it as "read-only" (a security reviewer will push backit is a replication privilege, just tightly scoped).
130
+ YOU WRITE THROUGH ABLO; ABLO WRITES TO YOUR POSTGRES. In production every schema model is backed by YOUR OWN database. You call `ablo.<model>.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 <admin-conn>` 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.
131
+
132
+ 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`.
127
133
 
128
- FALLBACK (only when the database can't grant a `REPLICATION` role, e.g. a locked-down managed DB): a signed Data Source endpoint that hands Ablo an ORM `adapter` (Drizzle default; Prisma and Kysely supported) — it owns the transaction, exactly-once idempotency, and outbox in ONE pass; your credentials never leave your infrastructure.
134
+ 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.
129
135
 
130
- DEPRECATED — being removed, do NOT recommend or scaffold: the `databaseUrl` constructor option (the dial-in, where Ablo held a read/write connection and committed directly to your DB). The end-state constructor is `Ablo({ schema, apiKey })`; the database is connected out of band via `ablo connect`, not through `databaseUrl`. The role rule you may have seen belongs ONLY to this dial-in: a `databaseUrl` had to name a NON-superuser, NON-BYPASSRLS role, or Ablo rejected it with `database_role_cannot_enforce_rls` (it enforced row-level security through the connection it held, so an owner/BYPASSRLS role e.g. Neon's `neondb_owner` would have silently skipped RLS). The WAL path retires that rule entirely: Ablo never holds a connection to your rows, so there is no `DATABASE_URL` to scope `ablo connect` needs a `REPLICATION` role instead. `ablo init` still scaffolds `--storage direct`/`databaseUrl` for existing dial-in projects and the CLI now warns on it; prefer `ablo connect` regardless of what `init` scaffolds.
136
+ DEPRECATED — removed, do NOT recommend or scaffold: the `databaseUrl` constructor option (the old DIAL-IN, where the CLIENT held a read/write connection string and committed directly, bypassing Ablo's coordination). It is deleted. DO NOT confuse it with the current model: today's writes also land in your Postgres, but through Ablo's SERVER-SIDE commit chokepoint and a scoped writer role Ablo provisions coordination is never bypassed, and no connection string lives in your app. The NON-superuser / NON-BYPASSRLS role rule is NOT retired it now applies to the scoped writer role `ablo connect` provisions (see RLS above), because Ablo does hold a scoped connection to write your rows. The end-state constructor is `Ablo({ schema, apiKey })`; the database is connected out of band via `ablo connect`. `ablo init` may still scaffold legacy `--storage direct`/`databaseUrl` for old projects and the CLI warns on it; prefer `ablo connect`.
131
137
 
132
138
  ```ts
133
139
  // app/api/ablo/source/route.ts
@@ -188,7 +194,7 @@ Do not teach `/api`, `/agent`, `/ai-sdk`, `/core`, `/realtime`, or internal subp
188
194
  `ablo init` and other prompts need a TTY; an agent/CI run has none and will HANG. Always:
189
195
 
190
196
  - `npx ablo init --yes` (flags: `--framework`, `--auth`, `--storage direct|endpoint`, `--no-agent`, `--no-pull`, `--no-install`, `--no-login`). Generates `ablo/schema.ts` + the `Ablo({ schema, apiKey })` client. `--storage endpoint` also scaffolds the `ablo/data-source.ts` fallback endpoint; `--storage direct` (deprecated, CLI warns) wires the legacy `databaseUrl`.
191
- - `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.
197
+ - `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`.
192
198
  - Key: see "Start here" — env → `.env.local` → ask the human to `npx ablo login`; never run `login` yourself, never copy keys by hand (`ablo push` writes `.env.local`).
193
199
  - Adopt an existing DB: `npx ablo pull prisma [path]` / `npx ablo pull drizzle <module>`.
194
200
  - `npx ablo push` pushes the schema (sandbox) AND writes `ABLO_API_KEY` to `.env.local`; `npx ablo dev --no-watch` is the push-once form of the watcher; `npx ablo logs --no-follow` exits instead of tailing forever; `npx ablo mode sandbox|production` always needs the argument. `npx ablo push`/`status`/`pull`/`check`/`generate` are one-shot.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.31.0",
3
+ "version": "0.33.0",
4
4
  "description": "The Collaboration Layer For AI Agents",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",