@abloatai/ablo 0.5.1 → 0.7.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 (129) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +248 -124
  3. package/dist/BaseSyncedStore.d.ts +3 -3
  4. package/dist/BaseSyncedStore.js +3 -3
  5. package/dist/api/index.d.ts +3 -3
  6. package/dist/api/index.js +1 -1
  7. package/dist/client/Ablo.d.ts +91 -93
  8. package/dist/client/Ablo.js +122 -60
  9. package/dist/client/ApiClient.d.ts +14 -14
  10. package/dist/client/ApiClient.js +81 -55
  11. package/dist/client/createInternalComponents.d.ts +2 -3
  12. package/dist/client/createInternalComponents.js +2 -3
  13. package/dist/client/createModelProxy.d.ts +116 -90
  14. package/dist/client/createModelProxy.js +128 -128
  15. package/dist/client/index.d.ts +6 -7
  16. package/dist/client/index.js +4 -5
  17. package/dist/client/validateAbloOptions.js +5 -5
  18. package/dist/coordination/index.d.ts +6 -0
  19. package/dist/coordination/index.js +6 -0
  20. package/dist/coordination/schema.d.ts +329 -0
  21. package/dist/coordination/schema.js +209 -0
  22. package/dist/core/QueryView.d.ts +4 -1
  23. package/dist/core/QueryView.js +1 -1
  24. package/dist/core/index.d.ts +2 -0
  25. package/dist/core/index.js +7 -0
  26. package/dist/core/query-utils.d.ts +7 -10
  27. package/dist/core/query-utils.js +2 -3
  28. package/dist/errorCodes.d.ts +264 -0
  29. package/dist/errorCodes.js +251 -0
  30. package/dist/errors.d.ts +59 -14
  31. package/dist/errors.js +73 -12
  32. package/dist/index.d.ts +11 -9
  33. package/dist/index.js +8 -12
  34. package/dist/interfaces/index.d.ts +2 -10
  35. package/dist/mutators/Transaction.d.ts +2 -2
  36. package/dist/mutators/Transaction.js +2 -2
  37. package/dist/mutators/mutateActions.d.ts +44 -0
  38. package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
  39. package/dist/mutators/readerActions.d.ts +32 -0
  40. package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
  41. package/dist/policy/index.d.ts +1 -1
  42. package/dist/policy/index.js +1 -1
  43. package/dist/policy/types.d.ts +31 -0
  44. package/dist/policy/types.js +15 -0
  45. package/dist/query/types.d.ts +1 -1
  46. package/dist/react/AbloProvider.d.ts +13 -1
  47. package/dist/react/AbloProvider.js +14 -6
  48. package/dist/react/context.d.ts +4 -4
  49. package/dist/react/index.d.ts +4 -5
  50. package/dist/react/index.js +3 -7
  51. package/dist/react/useAblo.d.ts +14 -14
  52. package/dist/react/useAblo.js +26 -26
  53. package/dist/react/useIntent.d.ts +2 -2
  54. package/dist/react/useIntent.js +2 -2
  55. package/dist/react/useMutators.d.ts +1 -1
  56. package/dist/react/usePresence.d.ts +3 -3
  57. package/dist/react/usePresence.js +4 -4
  58. package/dist/react/useUndoScope.d.ts +1 -1
  59. package/dist/schema/ddl.d.ts +62 -0
  60. package/dist/schema/ddl.js +317 -0
  61. package/dist/schema/diff.d.ts +167 -0
  62. package/dist/schema/diff.js +280 -0
  63. package/dist/schema/field.d.ts +16 -19
  64. package/dist/schema/field.js +30 -17
  65. package/dist/schema/generate.d.ts +19 -0
  66. package/dist/schema/generate.js +87 -0
  67. package/dist/schema/index.d.ts +9 -3
  68. package/dist/schema/index.js +14 -2
  69. package/dist/schema/model.d.ts +87 -25
  70. package/dist/schema/model.js +33 -3
  71. package/dist/schema/relation.d.ts +17 -0
  72. package/dist/schema/roles.d.ts +148 -0
  73. package/dist/schema/roles.js +149 -0
  74. package/dist/schema/schema.d.ts +10 -69
  75. package/dist/schema/schema.js +58 -24
  76. package/dist/schema/select.d.ts +25 -0
  77. package/dist/schema/select.js +55 -0
  78. package/dist/schema/serialize.d.ts +96 -0
  79. package/dist/schema/serialize.js +231 -0
  80. package/dist/schema/sugar.d.ts +20 -3
  81. package/dist/schema/sugar.js +5 -1
  82. package/dist/schema/tenancy.d.ts +66 -0
  83. package/dist/schema/tenancy.js +58 -0
  84. package/dist/sync/HydrationCoordinator.d.ts +2 -0
  85. package/dist/sync/HydrationCoordinator.js +23 -17
  86. package/dist/sync/SyncWebSocket.d.ts +17 -0
  87. package/dist/sync/SyncWebSocket.js +46 -1
  88. package/dist/sync/awaitIntentGrant.d.ts +26 -0
  89. package/dist/sync/awaitIntentGrant.js +60 -0
  90. package/dist/sync/createIntentStream.d.ts +2 -1
  91. package/dist/sync/createIntentStream.js +89 -5
  92. package/dist/sync/createPresenceStream.js +1 -1
  93. package/dist/sync/participants.d.ts +2 -2
  94. package/dist/sync/participants.js +9 -18
  95. package/dist/types/global.d.ts +43 -52
  96. package/dist/types/global.js +16 -18
  97. package/dist/types/streams.d.ts +90 -42
  98. package/docs/api-keys.md +44 -0
  99. package/docs/api.md +72 -173
  100. package/docs/audit.md +5 -5
  101. package/docs/cli.md +212 -0
  102. package/docs/client-behavior.md +42 -43
  103. package/docs/coordination.md +343 -0
  104. package/docs/data-sources.md +16 -16
  105. package/docs/examples/agent-human.md +30 -32
  106. package/docs/examples/ai-sdk-tool.md +32 -33
  107. package/docs/examples/existing-python-backend.md +38 -36
  108. package/docs/examples/nextjs.md +24 -25
  109. package/docs/examples/scoped-agent.md +78 -0
  110. package/docs/examples/server-agent.md +20 -61
  111. package/docs/guarantees.md +34 -56
  112. package/docs/identity.md +529 -0
  113. package/docs/index.md +18 -24
  114. package/docs/integration-guide.md +130 -144
  115. package/docs/interaction-model.md +32 -95
  116. package/docs/mcp/claude-code.md +3 -3
  117. package/docs/mcp/cursor.md +1 -1
  118. package/docs/mcp/windsurf.md +1 -1
  119. package/docs/mcp.md +11 -26
  120. package/docs/quickstart.md +43 -49
  121. package/docs/react.md +74 -24
  122. package/docs/roadmap.md +17 -7
  123. package/llms.txt +34 -39
  124. package/package.json +8 -1
  125. package/dist/react/useMutate.d.ts +0 -83
  126. package/dist/react/useQuery.d.ts +0 -123
  127. package/dist/react/useQuery.js +0 -145
  128. package/dist/react/useReader.d.ts +0 -69
  129. package/docs/capabilities.md +0 -163
package/CHANGELOG.md CHANGED
@@ -1,5 +1,66 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Structured error contract, schema/migration engine, and a full `ablo` CLI.
8
+ - **Structured error contract across HTTP + WS planes.** A closed, canonical
9
+ error-code registry is now the `code` tier of a Stripe-style error model. A
10
+ single HTTP egress funnel converts every throw to a canonical
11
+ `{ type, code, message, doc_url, request_id, ...details }` envelope; the WS
12
+ plane narrows mutation/claim error codes to the same union.
13
+ - **Versioned contract + drift guard.** `ERROR_CONTRACT_VERSION` (date-based)
14
+ ships in `errors.json` and on the `Ablo-Version` response header, so consumers
15
+ detect contract changes without diffing docs. Generated `errors.mdx` /
16
+ `errors.json` plus a CI drift guard keep the docs, OpenAPI spec, and SDK from
17
+ silently diverging from the registry.
18
+ - **Always-on request correlation.** Every response carries a `req_…` request id
19
+ (honoring an inbound `x-request-id`), stamped into the envelope's `request_id`.
20
+ - **OpenAPI parity.** The stale `{ error, reason }` schema is replaced by the
21
+ canonical envelope plus a generated `ErrorCode` enum.
22
+
23
+ CLI + schema:
24
+ - **Schema diff + migration planning engine** (`generateProvisionPlan` /
25
+ `generateMigrationPlan` in `@abloatai/ablo/schema`) — pure diff, classify,
26
+ apply, and constant-value backfill for required-field migrations.
27
+ - **`ablo generate`** — emit TypeScript types from the pushed schema.
28
+ - **Full `ablo` CLI suite**, Stripe-CLI-shaped: `init`, `login` / `logout` /
29
+ `status`, `mode [test|live]`, `dev` (push schema to the test sandbox + watch),
30
+ `logs` (tail your scope's commit activity), and the data-source commands below.
31
+ Authentication is the OAuth 2.0 device flow; `login` provisions and stores a
32
+ test and a live key, and `mode` switches the active one.
33
+ - **Database-URL structure (bring-your-own-database).** The CLI is split by where
34
+ it writes:
35
+ - `ablo pull` / `ablo check` / `ablo migrate` operate on **your own
36
+ `DATABASE_URL`** — `pull` introspects it to emit `defineSchema(...)` from
37
+ existing tables (read-only, like `prisma db pull`), `check` verifies tables
38
+ fit the schema with no DDL, and `migrate` applies DDL to `DATABASE_URL`.
39
+ - `ablo schema push` / `ablo dev` target the **hosted** test/live sandbox; the
40
+ server diffs, migrates, and activates the uploaded schema. `dev` never
41
+ touches live data.
42
+
43
+ **BREAKING** — removed the legacy React hooks `useQuery` / `useOne` / `useMutate`
44
+ / `useReader`. Use `useAblo()` + `ablo.<model>.*` instead. The `MutateActions`,
45
+ `ReaderActions`, and `ReaderFindOptions` types are still re-exported for callers
46
+ that referenced them.
47
+
48
+ ## 0.6.0
49
+
50
+ ### Minor Changes
51
+
52
+ - 0f663e7: Coordination surface: fair queue, reactive wait-line, and lease renewal.
53
+ - **Claims acquire through a server FIFO queue.** On contention a claim waits its turn and re-reads before proceeding; reads are never blocked. Writes blocked by another participant's claim throw a typed `AbloBusyError`.
54
+ - **`ablo.<model>.queue(id)`** — reactive read of the wait-line behind a row: who's queued, their action, and FIFO position. Synced to peers like `activity(id)`.
55
+ - **Backpressure on `claim`** — `{ wait: false }` skips instead of waiting if the row is already held (claim-or-skip dedup); `{ maxQueueDepth: n }` bails with `AbloBusyError('queue_too_deep')` rather than joining a line already that deep.
56
+ - **Lease renewal** — a held claim renews automatically while the holder's connection is alive, so you never size a TTL; it lapses only after the holder goes silent. A queued claim that's abandoned is dequeued (no ghost waiters).
57
+ - **Reads are never gated by a claim**, including for agents.
58
+ - Intent vocabulary cleanup: a waiting claim is an `Intent` with `status: 'queued'` (`position` carries its place in line). Removed the unbuilt `whenFree`.
59
+
60
+ - **BREAKING — API renames** (apply when upgrading from 0.5.1):
61
+ - Change-listeners renamed to `.onChange(...)`: `ablo.<model>.subscribe(cb)`, `presence.subscribe()`, `intents.subscribe()` → `.onChange(...)`. (`subscribe` is reserved for an upcoming scope-grant verb.)
62
+ - Row-access API renamed Resource → Model: `Ablo.Resource.*` → `Ablo.Model.*`, `ablo.resource(name)` → `ablo.model(name)`, `ModelTarget.resource` → `ModelTarget.model`, error code `resource_not_found` → `model_not_found`.
63
+
3
64
  ## 0.5.1
4
65
 
5
66
  ### Patch Changes
package/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # Ablo
2
2
 
3
- Ablo Sync is a typed sync engine for shared app state — the kind that humans,
3
+ [![npm](https://img.shields.io/npm/v/@abloatai/ablo.svg)](https://www.npmjs.com/package/@abloatai/ablo)
4
+ [![license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE)
5
+ [![types](https://img.shields.io/badge/types-included-blue.svg)](#)
6
+ [![runtime](https://img.shields.io/badge/node-%E2%89%A522-brightgreen.svg)](#keys--runtime)
7
+
8
+ Ablo is a typed sync engine for shared app state — the kind that humans,
4
9
  server code, and AI agents all edit at once.
5
10
 
6
11
  Reach for it when those edits need to show up everywhere in real time, not
@@ -8,28 +13,49 @@ silently overwrite each other, expose who's working on what, and leave a record
8
13
  of who changed what.
9
14
 
10
15
  ```txt
11
- schema -> ablo.<model>.create/retrieve/load/update/intent(...)
16
+ schema -> ablo.<model>.create/retrieve/update/claim(...)
12
17
  ```
13
18
 
14
- ## Install
19
+ ## Why Ablo
20
+
21
+ - **Real-time by default.** Every `create` / `update` / `delete` fans out
22
+ confirmed deltas to all subscribers — humans and agents — with no separate
23
+ "multiplayer mode" to switch on.
24
+ - **No silent clobbers.** Writes are guarded against stale reads, and `claim`
25
+ holds a row across a slow read → LLM → write gap so concurrent edits queue
26
+ instead of overwriting.
27
+ - **Built for agents.** See who's mid-edit (`claimState` / `queue`), coordinate a
28
+ fair line, and ship an `llms.txt` so coding agents integrate from the real API.
29
+ - **Typed end to end.** Your Zod schema produces typed model proxies
30
+ (`ablo.<model>.update(...)`), optimistic local reads, and reactive React hooks.
31
+ - **Bring your own auth and database.** Ablo scopes realtime data to *sync
32
+ groups* from your existing identity, and can leave your database as the source
33
+ of truth via a Data Source.
34
+
35
+ **Built for:** collaborative editors, AI agent workflows, internal tools, and any
36
+ app where multiple actors mutate shared state and everyone must see it live.
37
+
38
+ ## Set up
15
39
 
16
40
  ```bash
17
41
  npm install @abloatai/ablo
18
42
  ```
19
43
 
20
- Requires Node 22+ and TypeScript 5+.
21
-
22
- ## Get a Test Key
44
+ **Keys & runtime.** Ablo needs Node 22+ and TypeScript 5+. Grab an `sk_test_*`
45
+ key for a sandbox
46
+ (`export ABLO_API_KEY=sk_test_...`); keep keys in trusted server runtimes only.
47
+ In the browser, `<AbloProvider>` authenticates with the signed-in user's
48
+ session — never the raw key.
23
49
 
24
- Create an Ablo sandbox and copy an `sk_test_*` API key. Keep API keys in trusted
25
- server runtimes only.
50
+ Then wire it by hand the [Quick Start](#quick-start) below is the shape to
51
+ copy. For production (React, an existing backend, Data Source, agents), the
52
+ [Integration Guide](./docs/integration-guide.md) is the deeper map.
26
53
 
27
- ```bash
28
- export ABLO_API_KEY=sk_test_...
29
- ```
54
+ **Prefer to let an agent wire it?** The package ships an `llms.txt` — a precise
55
+ map of the API — so Claude Code or Cursor integrates from the real surface
56
+ instead of guessing:
30
57
 
31
- In the browser, connect through the React provider (`<AbloProvider>`), which
32
- authenticates with the signed-in user's session — never the raw API key.
58
+ > Read `node_modules/@abloatai/ablo/llms.txt`, then add an Ablo schema, a `<AbloProvider>`, and my first create / retrieve / update.
33
59
 
34
60
  ## Quick Start
35
61
 
@@ -73,9 +99,104 @@ Expected output:
73
99
  { id: '...', status: 'ready' }
74
100
  ```
75
101
 
76
- Pass `schema` to get typed models like `ablo.weatherReports.update(...)`. Omit it
77
- only for the lower-level client used by custom agents and MCP routes that can't
78
- import your app's schema.
102
+ Pass `schema` to get typed models like `ablo.weatherReports.update(...)`.
103
+
104
+ ## Reading
105
+
106
+ `retrieve(id)` returns one row from the local cache — synchronous, no round-trip.
107
+ `list(...)` filters and sorts what's already synced; it's also synchronous, and
108
+ reactive under `useAblo`/`subscribe`. `load(...)` fetches from the server when a
109
+ row may not be local yet.
110
+
111
+ ```ts
112
+ ablo.weatherReports.retrieve('report_stockholm');
113
+
114
+ const pending = ablo.weatherReports.list({
115
+ where: { status: 'pending' },
116
+ orderBy: { location: 'asc' },
117
+ limit: 20,
118
+ });
119
+
120
+ const ready = await ablo.weatherReports.load({
121
+ where: { status: 'ready' },
122
+ type: 'complete',
123
+ });
124
+ ```
125
+
126
+ An array value in `where` means `IN`. On `load`, `type: 'complete'` waits for
127
+ the server; `'unknown'` returns what's local now and refreshes in the background.
128
+
129
+ ## Writing
130
+
131
+ `create` / `update` apply optimistically and resolve to the row. Two options
132
+ matter day to day:
133
+
134
+ | Option | Values | What it does |
135
+ | --- | --- | --- |
136
+ | `wait` | `'queued'` \| `'confirmed'` | `'confirmed'` resolves only after the server acks the write; `'queued'` resolves as soon as it's locally queued (fire-and-forget). |
137
+ | `idempotencyKey` | `string` | Auto-generated per call. Override only when you own the retry boundary (e.g. a job id) so a re-run dedupes server-side. |
138
+
139
+ ```ts
140
+ await ablo.weatherReports.update(id, { status: 'ready' }, { wait: 'confirmed' });
141
+ ```
142
+
143
+ To guard a write against a row that changed under you, pass `readAt` + `onStale`
144
+ — see [Coordinating long agent work](#coordinating-long-agent-work).
145
+
146
+ ## Coordinating long agent work
147
+
148
+ An agent reads a row, thinks for 30s, writes back — and clobbers whatever changed
149
+ meanwhile, or worse, acts on stale state. `claim` holds the row across that gap:
150
+
151
+ ```ts
152
+ await ablo.weatherReports.claim('report_stockholm', async (report) => {
153
+ const forecast = await weatherAgent.getWeather(report.location);
154
+ await ablo.weatherReports.update(report.id, { forecast, status: 'ready' });
155
+ });
156
+ ```
157
+
158
+ If someone else holds the row, `claim()` waits in a fair queue, then re-reads —
159
+ so `report` is the current row, never a stale snapshot. Reads stay open by
160
+ default; only acting on the row serializes. The claim releases when the callback
161
+ returns or throws.
162
+
163
+ See who's mid-edit before you act — decide to wait, or skip:
164
+
165
+ ```ts
166
+ ablo.weatherReports.claimState('report_stockholm');
167
+ ablo.weatherReports.queue('report_stockholm');
168
+
169
+ await ablo.weatherReports.claim(id, async (report) => {
170
+ /* do the held work */
171
+ }, { wait: false });
172
+
173
+ await ablo.weatherReports.claim(id, async (report) => {
174
+ /* do the held work */
175
+ }, { maxQueueDepth: 2 });
176
+ ```
177
+
178
+ `claimState` returns the holder (or `null`); `queue` returns the line waiting
179
+ behind it. `wait: false` skips rather than waiting when the row is held;
180
+ `maxQueueDepth: 2` bails when two or more are already ahead.
181
+
182
+ Default reads keep working while a row is claimed. Server reads that need claimed
183
+ semantics can opt in with `ifClaimed: 'return' | 'wait' | 'fail'`.
184
+
185
+ Even an unclaimed write can't land on stale reasoning — the commit is guarded:
186
+
187
+ ```ts
188
+ try {
189
+ await ablo.weatherReports.update(id, { status: 'ready' }, { readAt, onStale: 'reject' });
190
+ } catch (e) {
191
+ if (e instanceof AbloStaleContextError) { /* row moved under you — re-read, retry */ }
192
+ }
193
+ ```
194
+
195
+ > Prefer the callback form for ordinary held work. Manual scoped claims are
196
+ > available for wider lifetimes, but callback claims are the docs default.
197
+
198
+ See [Coordination](./docs/coordination.md) for the full `claim` / `claimState` /
199
+ `queue` / `release` reference.
79
200
 
80
201
  ## React
81
202
 
@@ -85,7 +206,7 @@ everything inside is live.
85
206
 
86
207
  ```tsx
87
208
  import { AbloProvider, useAblo } from '@abloatai/ablo/react';
88
- import { schema } from './ablo.schema';
209
+ import { schema } from './ablo/schema';
89
210
 
90
211
  function App() {
91
212
  return (
@@ -96,14 +217,11 @@ function App() {
96
217
  }
97
218
 
98
219
  function Report({ id }: { id: string }) {
99
- // Reactive read: this re-renders whenever the row changes — whether you,
100
- // a teammate, or an agent changed it.
101
220
  const report = useAblo((ablo) => ablo.weatherReports.retrieve(id));
102
221
  const ablo = useAblo();
103
222
 
104
223
  if (!report) return null;
105
224
 
106
- // Write: same method as the server example above. Optimistic; fans out.
107
225
  return (
108
226
  <button onClick={() => ablo?.weatherReports.update(id, { status: 'ready' })}>
109
227
  {report.status}
@@ -112,62 +230,44 @@ function Report({ id }: { id: string }) {
112
230
  }
113
231
  ```
114
232
 
115
- `<AbloProvider>` owns the connection and authenticates with the signed-in user's
116
- session no API key in the browser. That's the whole loop: read with
117
- `useAblo(selector)`, write with `ablo.<model>`, and every other client (human or
118
- agent) on that row sees it in real time. See [React](./docs/react.md) for
119
- `fallback`, `bootstrapMode`, presence, and status hooks.
233
+ The `useAblo(selector)` read re-renders whenever the row changes whether you,
234
+ a teammate, or an agent changed it. The write is the same optimistic, fan-out
235
+ method as the server example above.
120
236
 
121
- ## Set up with Claude Code
237
+ `<AbloProvider>` owns the connection no API key in the browser. That's the
238
+ whole loop: read with `useAblo(selector)`, write with `ablo.<model>`, and every
239
+ other client (human or agent) on that row sees it in real time. See
240
+ [React](./docs/react.md) for the full `<AbloProvider>` prop surface (`userId`,
241
+ `teamIds`, `syncGroups`, `fallback`, `bootstrapMode`) and status hooks.
122
242
 
123
- The package ships an `llms.txt` — a compact, LLM-readable map of the whole API.
124
- Point your coding agent at it and let it do the integration:
243
+ ## Identity & Sync Groups
125
244
 
126
- > Read `node_modules/@abloatai/ablo/llms.txt`, then add an Ablo schema and
127
- > `<AbloProvider>` to this app and wire up my first create / retrieve / update.
245
+ Ablo is **not** an auth provider you keep your own (Clerk, Auth0, NextAuth,
246
+ whatever). Ablo's job starts after you've authenticated a request: you tell it
247
+ *who* is connecting, and it scopes their realtime data to the right **sync
248
+ groups** (named channels like `org:acme` or `deck:abc123` that are both the unit
249
+ of fan-out and the unit of access).
128
250
 
129
- It scaffolds the schema, sets up the provider, and writes your first
130
- `ablo.<model>` calls the same shape as the Quick Start above.
251
+ The model is a proxy: your `ABLO_API_KEY` stays on your trusted server, your
252
+ server resolves the signed-in user (org / team / user) from your own auth, and
253
+ the browser connects as an already-scoped participant — it never holds the key
254
+ and can't widen its own scope. Your schema's `identityRoles` map that identity
255
+ to sync-group strings.
131
256
 
132
- For a production integration with React, an existing backend, Data Source, and
133
- future agents, read [Integration Guide](./docs/integration-guide.md).
257
+ `userId` / `teamIds` come from your auth, resolved server-side:
134
258
 
135
- ## AI Activity on Existing State
136
-
137
- When AI or background work will spend real time on an existing row — not just a
138
- one-shot write — coordinate through `ablo.<model>.intent(id)`, the coordination
139
- accessor that sits beside `create`/`update`/`retrieve`. It takes the row's `id` (the same
140
- id you pass to `retrieve(id)` / `update(id, …)`) and returns a handle
141
- synchronously, so you can see who's already working on a row before you start.
142
-
143
- ```ts
144
- // `report_stockholm` is this weather report's id — set at create time, or the
145
- // `created.id` returned above.
146
- const report = ablo.weatherReports.intent('report_stockholm');
147
-
148
- // Read side: is someone already on it? Wait for them to finish.
149
- if (report.current) {
150
- report.current.heldBy; // 'agent:forecaster'
151
- await report.whenFree();
152
- }
153
-
154
- // Write side: claim so other participants yield while we work.
155
- await report.claim({ action: 'checking_weather', field: 'forecast', ttl: '2m' });
156
-
157
- // Your existing weather tool or agent call. While this runs, other clients see
158
- // that report_stockholm is being checked.
159
- const row = ablo.weatherReports.retrieve('report_stockholm');
160
- const weather = await weatherAgent.getWeather(row.location);
161
-
162
- await report.update({
163
- status: 'ready',
164
- forecast: weather.summary,
165
- });
259
+ ```tsx
260
+ <AbloProvider schema={schema} userId={user.id} teamIds={user.teamIds}>
261
+ <App />
262
+ </AbloProvider>
166
263
  ```
167
264
 
168
- Ablo does not fetch the weather. It keeps the activity visible while the work
169
- runs, rejects `report.update(...)` with `AbloStaleContextError` if the row
170
- changed under you, and finishes the claim automatically once the write lands.
265
+ If it isn't obvious where org / team / user come from in the Quick Start above,
266
+ that's because they come from *your* app — see
267
+ [Identity & Sync Groups](./docs/identity.md) for the full picture: what a sync
268
+ group is, the two halves of scoping (`identityRoles` + per-model `orgScoped` /
269
+ `syncGroupFormat`), and how identity reaches Ablo without an API key in the
270
+ browser.
171
271
 
172
272
  ## Multiplayer
173
273
 
@@ -176,63 +276,32 @@ workers share the same schema and write through `ablo.<model>`, they all see
176
276
  each other's changes in real time — that's the default, not a feature you turn on.
177
277
 
178
278
  - `ablo.<model>.create/update/delete` fan out confirmed deltas to subscribers.
179
- - `useAblo(...)` gives React clients the live row plus active intents.
180
- - `ablo.<model>.intent(id)` lets humans and agents see and coordinate active work before a write lands.
181
-
182
- Always write through Ablo — the SDK (`ablo.<model>.create/update/delete`) or the
183
- HTTP API (`POST /v1/commits`). If you write straight to your own database
184
- instead, those changes won't reach connected clients. Use Ablo's endpoints and
185
- the fan-out is automatic.
186
-
187
- Under the hood, capabilities, tasks, leases, intents, commits, and receipts are
188
- real protocol primitives. They exist so agent work is scoped, coordinated,
189
- attributable, and cleaned up if a runtime disappears — but you don't touch them
190
- in a first integration. The default path above is enough.
191
-
192
- ## HTTP API
193
-
194
- The SDK is a typed wrapper over a signed HTTP API, the same way `stripe-node`
195
- wraps Stripe's REST API. Everything you do through `ablo.<model>` is reachable
196
- over HTTP, so backends in other languages and server-to-server callers work
197
- without the SDK.
198
-
199
- Writes — create, update, and delete — all go through one endpoint as a batch of
200
- operations:
201
-
202
- ```http
203
- POST /v1/commits
204
- Authorization: Bearer sk_live_...
205
- Idempotency-Key: <your-unique-id>
206
-
207
- {
208
- "operations": [
209
- {
210
- "type": "UPDATE",
211
- "model": "weatherReports",
212
- "id": "report_stockholm",
213
- "input": { "status": "ready" },
214
- "readAt": 1042,
215
- "onStale": "reject"
216
- }
217
- ]
218
- }
219
- ```
279
+ - `useAblo(...)` gives React clients the live row, kept current automatically.
280
+ - `ablo.<model>.claim(id)` / `claimState(id)` / `queue(id)` let humans and agents coordinate (and observe) active work on a row — and the line waiting behind it — before a write lands.
220
281
 
221
- Each operation is one `CREATE` / `UPDATE` / `DELETE` (plus `ARCHIVE` /
222
- `UNARCHIVE`). Reads fetch a single row with `GET /v1/resources/{model}/{id}`.
223
- The same API key, scope, idempotency, and stale-write rules apply as in the SDK
224
- — the SDK just removes the boilerplate.
282
+ Always write through Ablo either the SDK model methods
283
+ (`ablo.<model>.create/update/delete`) or the HTTP write endpoint below. If you
284
+ write straight to your own database instead, those changes won't reach connected
285
+ clients.
225
286
 
226
- ## Load vs Retrieve
287
+ ## HTTP Writes
227
288
 
228
- Most reads are `retrieve` it's the everyday path, especially inside `useAblo(...)`:
289
+ Use the SDK when you are in JavaScript and want typed models or realtime. Use the
290
+ HTTP endpoint when a server-to-server caller needs to write without opening a
291
+ WebSocket:
229
292
 
230
- - `ablo.weatherReports.retrieve(id)` is sync. It returns the already-loaded row from
231
- the local pool (or `undefined` if it isn't loaded yet). In React,
232
- `useAblo((ablo) => ablo.weatherReports.retrieve(id))` keeps that read live.
233
- - `ablo.weatherReports.load({ where })` is async. Reach for it when you need to
234
- guarantee a row is hydrated before you read it — initial fetch, SSR, scripts, or
235
- any non-reactive runtime.
293
+ ```bash
294
+ curl https://api.abloatai.com/v1/commits \
295
+ -H "Authorization: Bearer sk_test_..." \
296
+ -H "Content-Type: application/json" \
297
+ -d '{ "operations": [
298
+ { "action": "update", "model": "weatherReports", "id": "report_stockholm", "data": { "status": "ready" } }
299
+ ] }'
300
+ ```
301
+
302
+ ```json
303
+ { "object": "commit_receipt", "status": "confirmed", "serverTxId": "tx_…", "lastSyncId": 1042, "ops": 1 }
304
+ ```
236
305
 
237
306
  ## Connect Your Database
238
307
 
@@ -245,18 +314,73 @@ Source: Ablo sends signed commit requests to an endpoint you host, and your app
245
314
  writes its own database. Your `DATABASE_URL` stays in your app — Ablo only ever
246
315
  sees the API key.
247
316
 
248
- See [Connect Your Database](./docs/data-sources.md) for the route and commit shape.
317
+ See [Connect Your Database](./docs/data-sources.md) for the integration shape.
318
+
319
+ ## Configuration
320
+
321
+ `Ablo({ ... })` takes one required option and a couple of transport overrides:
322
+
323
+ | Option | Type | Default | Purpose |
324
+ | --- | --- | --- | --- |
325
+ | `schema` | `Schema` | — (required) | Typed model proxies (`ablo.<model>.*`) |
326
+ | `apiKey` | `string \| ApiKeySetter \| null` | `process.env.ABLO_API_KEY` | Server key — a string, or an async function for rotation |
327
+ | `baseURL` | `string` | `wss://mesh.ablo.finance` | Point at a self-hosted or staging mesh |
328
+
329
+ Keep `apiKey` in trusted server runtimes. In the browser, `<AbloProvider>`
330
+ authenticates with the signed-in user's session; the raw-key path is gated
331
+ behind `dangerouslyAllowBrowser` for server-proxy setups only. Self-hosted
332
+ deployments can pass `authToken` instead of `apiKey`. Advanced hooks (custom
333
+ `fetch`, logging, observability) live in [Client Behavior](./docs/client-behavior.md).
334
+
335
+ ## Errors
336
+
337
+ Every SDK error extends `AbloError` and carries a `requestId` for support.
338
+ Discriminate with `instanceof` or the `type` string — the string form also
339
+ survives worker / `postMessage` boundaries, where `instanceof` does not:
340
+
341
+ ```ts
342
+ try {
343
+ await ablo.weatherReports.update(id, { status: 'ready' }, { readAt, onStale: 'reject' });
344
+ } catch (e) {
345
+ if (e instanceof AbloStaleContextError) { /* row moved under you — re-read, retry */ }
346
+ if ((e as AbloError).type === 'AbloClaimedError') { /* another participant holds it */ }
347
+ }
348
+ ```
349
+
350
+ | Error | When |
351
+ | --- | --- |
352
+ | `AbloAuthenticationError` | Invalid / missing / expired credentials |
353
+ | `AbloPermissionError` / `CapabilityError` | Action forbidden by scope |
354
+ | `AbloRateLimitError` | Rate limited (carries `retryAfterSeconds`) |
355
+ | `AbloIdempotencyError` | Same `idempotencyKey` reused with a different body |
356
+ | `AbloValidationError` | Invalid request payload |
357
+ | `AbloStaleContextError` | Write carried `readAt`, but the row has newer changes (`conflicts`) |
358
+ | `AbloClaimedError` | Target is claimed by another participant (`claims`) |
359
+ | `AbloConnectionError` / `AbloServerError` | Transport failure / server 5xx |
360
+ | `SyncSessionError` | Session expired (prompts re-auth) |
361
+
362
+ ## Reconnect & retries
363
+
364
+ The client owns reconnection so your code doesn't have to. A dropped WebSocket
365
+ reconnects automatically with exponential backoff (1s → 30s, ±15% jitter, up to
366
+ ~7.5 minutes); session errors (401/403) suppress it so you re-authenticate
367
+ instead of looping. Commits are idempotent by client transaction id, and a
368
+ commit that times out is never silently rolled back — the client reconciles
369
+ against authoritative server state on reconnect. These defaults are the
370
+ contract; there are no retry or timeout knobs to tune.
249
371
 
250
372
  ## Production Reference
251
373
 
252
- - [Guarantees](./docs/guarantees.md) — confirmed writes, stale-write protection, intent coordination, and agent lifecycle.
374
+ - [Identity & Sync Groups](./docs/identity.md) — bring your own auth; tell Ablo who's connecting and how org / team / user map to sync-group scope.
375
+ - [Guarantees](./docs/guarantees.md) — confirmed writes, stale-write protection, claim coordination, and agent lifecycle.
253
376
  - [Integration Guide](./docs/integration-guide.md) — pick the backing mode and integrate React, Data Source, multiplayer, and agents.
254
377
  - [React](./docs/react.md) — `<AbloProvider>`, `useAblo`, presence, status, and bootstrap gating.
378
+ - [Coordination](./docs/coordination.md) — `claim` / `claimState` / `queue` / `release` reference: hold a row across slow agent work, and observe the line waiting behind it.
255
379
  - [Client Behavior](./docs/client-behavior.md) — options, errors, retries, timeouts, and public imports.
256
380
  - [Connect Your Database](./docs/data-sources.md) — keep canonical rows in your app database without giving Ablo database credentials.
257
381
  - [Existing Python Backend](./docs/examples/existing-python-backend.md) — migrate existing Python endpoints to multiplayer and agent-safe writes gradually.
258
382
  - [AI SDK Tool](./docs/examples/ai-sdk-tool.md) — use Ablo inside an AI SDK tool call.
259
- - [Server Agent](./docs/examples/server-agent.md) — schema-backed worker plus advanced schema-less run.
383
+ - [Server Agent](./docs/examples/server-agent.md) — schema-backed worker.
260
384
 
261
385
  ## License
262
386
 
@@ -21,7 +21,7 @@ import { QueryProcessor } from './core/QueryProcessor.js';
21
21
  import { Model } from './Model.js';
22
22
  import { ModelScope } from './ObjectPool.js';
23
23
  import type { Schema } from './schema/schema.js';
24
- import { type ReaderActions } from './react/useReader.js';
24
+ import { type ReaderActions } from './mutators/readerActions.js';
25
25
  /** Constructor type for Model subclasses (accepts abstract classes) */
26
26
  export type ModelConstructor<T extends Model> = abstract new (...args: never[]) => T;
27
27
  /** Concrete constructor type for instantiation */
@@ -139,7 +139,7 @@ export interface UserContext {
139
139
  * - `'full'` (default): pull every delta in scope before `ready()`
140
140
  * resolves. The standard browser/user replica behavior.
141
141
  * - `'none'`: open the WebSocket and process live deltas only.
142
- * Reads go through `resource.retrieve()` / filtered subscriptions
142
+ * Reads go through `model.retrieve()` / filtered subscriptions
143
143
  * backfilled by `Covering` deltas. Suitable for transactional
144
144
  * participants — agent-worker, video-pipeline, routine runners —
145
145
  * that don't need a local replica of the org's tenant plane.
@@ -662,7 +662,7 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
662
662
  */
663
663
  queryByClass(modelClass: ModelConstructor<Model>, options?: {
664
664
  predicate?: (model: Model) => boolean;
665
- scope?: ModelScope;
665
+ state?: ModelScope;
666
666
  orderBy?: keyof Model;
667
667
  order?: 'asc' | 'desc';
668
668
  limit?: number;
@@ -22,7 +22,7 @@ import { getContext } from './context.js';
22
22
  import { SyncSessionError } from './errors.js';
23
23
  import { ModelScope } from './ObjectPool.js';
24
24
  import { LazyReferenceCollection } from './LazyReferenceCollection.js';
25
- import { createReaderActions } from './react/useReader.js';
25
+ import { createReaderActions } from './mutators/readerActions.js';
26
26
  /** Bootstrap timeout configuration */
27
27
  export const BOOTSTRAP_CONFIG = {
28
28
  OVERALL_TIMEOUT_MS: 15_000,
@@ -790,7 +790,7 @@ export class BaseSyncedStore {
790
790
  //
791
791
  // `bootstrapMode: 'none'` participants (agent-worker, headless
792
792
  // task runners) skip baseline replication — they read via
793
- // `resource.retrieve()` round-trips and rely on covering deltas
793
+ // `model.retrieve()` round-trips and rely on covering deltas
794
794
  // from filtered subscriptions to populate the pool lazily. The
795
795
  // WS is already open by `setupWebSocketSync` above, so live
796
796
  // delta flow works regardless of this branch.
@@ -1688,7 +1688,7 @@ export class BaseSyncedStore {
1688
1688
  const modelName = this.objectPool.registry.getModelNameFromConstructor(modelClass);
1689
1689
  if (!modelName)
1690
1690
  return { data: [], total: 0, hasMore: false };
1691
- let allModels = this.objectPool.getByType(modelClass, options?.scope ?? ModelScope.live);
1691
+ let allModels = this.objectPool.getByType(modelClass, options?.state ?? ModelScope.live);
1692
1692
  // Filter out pending deletes
1693
1693
  allModels = allModels.filter((m) => !this.pendingDeletes.has(m.id));
1694
1694
  // Apply predicate
@@ -2,9 +2,9 @@
2
2
  * Internal compatibility entrypoint for the stateless hosted protocol client.
3
3
  *
4
4
  * Use this build for serverless functions, scripts, and backends that want
5
- * Resource / Intent / Commit over HTTP without the realtime sync runtime.
5
+ * model reads/writes and commits over HTTP without the realtime sync runtime.
6
6
  */
7
- export { createProtocolClient, createProtocolClient as Ablo, type AbloApi, type AbloApiClientOptions, type AbloApiIntents, type Agent, type AgentIntentInput, type AgentIntentOptions, type AgentOptions, type AgentResourceClient, type AgentResourceReadOptions, type AgentResourceMutationOptions, type AgentRunContext, type AgentRunDone, type AgentRunFailed, type AgentRunCancelled, type AgentRunOptions, type AgentRunResult, type AgentRunStatus, type Capability, type CapabilityCreateOptions, type CapabilityParticipantKind, type CapabilityRecord, type CapabilityResource, type CapabilityRevocation, type CapabilityScope, type Task, type TaskCloseOptions, type TaskCloseResult, type TaskCreateOptions, type TaskResource, } from '../client/ApiClient.js';
8
- export type { CommitCreateOptions, CommitOperationInput, CommitReceipt, CommitWait, IntentCreateOptions, IntentHandle, IntentWaitOptions, BusyOptions, BusyPolicy, ResourceClient, ResourceIntent, ResourceMutationOptions, ResourceReadOptions, ResourceRead, ResourceTarget, } from '../client/Ablo.js';
7
+ export { createProtocolClient, createProtocolClient as Ablo, type AbloApi, type AbloApiClientOptions, type AbloApiIntents, type Agent, type AgentIntentInput, type AgentIntentOptions, type AgentOptions, type AgentModelClient, type AgentModelReadOptions, type AgentModelMutationOptions, type AgentRunContext, type AgentRunDone, type AgentRunFailed, type AgentRunCancelled, type AgentRunOptions, type AgentRunResult, type AgentRunStatus, type Capability, type CapabilityCreateOptions, type CapabilityParticipantKind, type CapabilityRecord, type CapabilityResource, type CapabilityRevocation, type CapabilityScope, type Task, type TaskCloseOptions, type TaskCloseResult, type TaskCreateOptions, type TaskResource, } from '../client/ApiClient.js';
8
+ export type { CommitCreateOptions, CommitOperationInput, CommitReceipt, CommitWait, IntentCreateOptions, IntentHandle, IntentWaitOptions, ClaimedOptions, IfClaimedPolicy, ModelClient, ModelClaim, ModelMutationOptions, ModelReadOptions, ModelRead, ModelTarget, } from '../client/Ablo.js';
9
9
  import { createProtocolClient } from '../client/ApiClient.js';
10
10
  export default createProtocolClient;
package/dist/api/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Internal compatibility entrypoint for the stateless hosted protocol client.
3
3
  *
4
4
  * Use this build for serverless functions, scripts, and backends that want
5
- * Resource / Intent / Commit over HTTP without the realtime sync runtime.
5
+ * model reads/writes and commits over HTTP without the realtime sync runtime.
6
6
  */
7
7
  export { createProtocolClient, createProtocolClient as Ablo, } from '../client/ApiClient.js';
8
8
  import { createProtocolClient } from '../client/ApiClient.js';