@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.
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +50 -0
- package/README.md +25 -13
- package/dist/cli.cjs +1686 -1219
- package/dist/coordination/schema.d.ts +4 -2
- package/dist/coordination/schema.js +23 -9
- package/dist/errorCodes.d.ts +1 -0
- package/dist/errorCodes.js +2 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +1 -1
- package/dist/schema/schema.d.ts +55 -1
- package/dist/schema/schema.js +47 -0
- package/dist/schema/select.js +1 -0
- package/dist/schema/serialize.d.ts +3 -1
- package/dist/schema/serialize.js +9 -1
- package/dist/source/adapters/drizzle.js +4 -3
- package/dist/source/adapters/kysely.js +4 -4
- package/dist/source/adapters/prisma.js +3 -3
- package/dist/source/idempotency.d.ts +19 -4
- package/dist/source/idempotency.js +19 -4
- package/dist/source/migrations.d.ts +4 -1
- package/dist/source/migrations.js +36 -5
- package/docs/agent-messaging.md +1 -2
- package/docs/api.md +4 -4
- package/docs/audit.md +35 -23
- package/docs/client-behavior.md +7 -4
- package/docs/concurrency-convention.md +3 -3
- package/docs/coordination.md +13 -1
- package/docs/data-sources.md +34 -32
- package/docs/examples/agent-human.md +2 -2
- package/docs/examples/ai-sdk-tool.md +1 -1
- package/docs/examples/nextjs.md +1 -1
- package/docs/examples/server-agent.md +2 -2
- package/docs/groups.md +160 -0
- package/docs/guarantees.md +9 -7
- package/docs/how-it-works.md +110 -0
- package/docs/integration-guide.md +2 -2
- package/docs/interaction-model.md +3 -3
- package/docs/quickstart.md +6 -6
- package/docs/webhooks.md +23 -22
- package/llms.txt +10 -4
- 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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
- `
|
|
91
|
-
- `
|
|
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
|
|
package/docs/quickstart.md
CHANGED
|
@@ -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
|
|
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
|
|
118
|
-
# npx ablo connect
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
9
|
-
database
|
|
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
|
-
|
|
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
|
+

|
|
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>"
|
|
59
|
-
| `model` | the model name
|
|
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
|
-
|
|
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
|
|
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 —
|
|
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
|
|
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.
|