@abloatai/ablo 0.6.0 → 0.8.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 (121) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +95 -57
  3. package/dist/BaseSyncedStore.d.ts +1 -1
  4. package/dist/BaseSyncedStore.js +8 -4
  5. package/dist/SyncEngineContext.d.ts +2 -1
  6. package/dist/SyncEngineContext.js +5 -3
  7. package/dist/agent/session.js +3 -2
  8. package/dist/auth/index.js +39 -11
  9. package/dist/client/Ablo.d.ts +112 -3
  10. package/dist/client/Ablo.js +144 -10
  11. package/dist/client/ApiClient.d.ts +32 -0
  12. package/dist/client/ApiClient.js +76 -44
  13. package/dist/client/auth.d.ts +11 -1
  14. package/dist/client/auth.js +21 -2
  15. package/dist/client/createModelProxy.d.ts +120 -53
  16. package/dist/client/createModelProxy.js +66 -31
  17. package/dist/client/identity.js +14 -0
  18. package/dist/client/registerDataSource.d.ts +19 -0
  19. package/dist/client/registerDataSource.js +57 -0
  20. package/dist/client/validateAbloOptions.d.ts +2 -1
  21. package/dist/client/validateAbloOptions.js +8 -7
  22. package/dist/coordination/index.d.ts +6 -0
  23. package/dist/coordination/index.js +6 -0
  24. package/dist/coordination/schema.d.ts +329 -0
  25. package/dist/coordination/schema.js +209 -0
  26. package/dist/core/QueryView.d.ts +4 -1
  27. package/dist/core/QueryView.js +1 -1
  28. package/dist/core/query-utils.d.ts +7 -10
  29. package/dist/core/query-utils.js +2 -3
  30. package/dist/errorCodes.d.ts +286 -0
  31. package/dist/errorCodes.js +284 -0
  32. package/dist/errors.d.ts +103 -7
  33. package/dist/errors.js +192 -41
  34. package/dist/index.d.ts +11 -6
  35. package/dist/index.js +10 -6
  36. package/dist/keys/index.d.ts +61 -0
  37. package/dist/keys/index.js +151 -0
  38. package/dist/policy/index.d.ts +1 -1
  39. package/dist/policy/index.js +1 -1
  40. package/dist/policy/types.d.ts +31 -0
  41. package/dist/policy/types.js +15 -0
  42. package/dist/query/client.js +19 -8
  43. package/dist/react/AbloProvider.d.ts +37 -0
  44. package/dist/react/AbloProvider.js +107 -4
  45. package/dist/react/ClientSideSuspense.d.ts +1 -1
  46. package/dist/react/DefaultFallback.d.ts +1 -1
  47. package/dist/react/SyncGroupProvider.d.ts +1 -1
  48. package/dist/react/index.d.ts +3 -2
  49. package/dist/react/index.js +3 -2
  50. package/dist/react/useAblo.d.ts +4 -4
  51. package/dist/react/useAblo.js +10 -5
  52. package/dist/react/useReactive.js +16 -3
  53. package/dist/schema/ddl.d.ts +62 -0
  54. package/dist/schema/ddl.js +317 -0
  55. package/dist/schema/diff.d.ts +6 -0
  56. package/dist/schema/diff.js +21 -3
  57. package/dist/schema/field.d.ts +16 -19
  58. package/dist/schema/field.js +30 -17
  59. package/dist/schema/index.d.ts +7 -4
  60. package/dist/schema/index.js +9 -3
  61. package/dist/schema/model.d.ts +87 -25
  62. package/dist/schema/model.js +33 -3
  63. package/dist/schema/relation.d.ts +17 -0
  64. package/dist/schema/roles.d.ts +148 -0
  65. package/dist/schema/roles.js +149 -0
  66. package/dist/schema/schema.d.ts +2 -112
  67. package/dist/schema/schema.js +50 -62
  68. package/dist/schema/select.d.ts +25 -0
  69. package/dist/schema/select.js +55 -0
  70. package/dist/schema/serialize.d.ts +16 -12
  71. package/dist/schema/serialize.js +16 -12
  72. package/dist/schema/sugar.d.ts +20 -3
  73. package/dist/schema/sugar.js +5 -1
  74. package/dist/schema/tenancy.d.ts +66 -0
  75. package/dist/schema/tenancy.js +58 -0
  76. package/dist/sync/BootstrapHelper.js +46 -27
  77. package/dist/sync/ConnectionManager.d.ts +3 -1
  78. package/dist/sync/ConnectionManager.js +37 -1
  79. package/dist/sync/HydrationCoordinator.d.ts +2 -0
  80. package/dist/sync/HydrationCoordinator.js +26 -19
  81. package/dist/sync/NetworkProbe.d.ts +8 -0
  82. package/dist/sync/NetworkProbe.js +24 -2
  83. package/dist/sync/SyncWebSocket.d.ts +1 -1
  84. package/dist/sync/SyncWebSocket.js +43 -53
  85. package/dist/sync/createIntentStream.d.ts +2 -1
  86. package/dist/sync/createIntentStream.js +46 -1
  87. package/dist/sync/participants.js +10 -16
  88. package/dist/transactions/TransactionQueue.js +13 -1
  89. package/dist/types/streams.d.ts +53 -33
  90. package/docs/api-keys.md +47 -3
  91. package/docs/api.md +103 -57
  92. package/docs/audit.md +16 -9
  93. package/docs/cli.md +222 -0
  94. package/docs/client-behavior.md +35 -21
  95. package/docs/coordination.md +74 -36
  96. package/docs/data-sources.md +23 -21
  97. package/docs/examples/agent-human.md +72 -28
  98. package/docs/examples/ai-sdk-tool.md +14 -11
  99. package/docs/examples/existing-python-backend.md +30 -19
  100. package/docs/examples/nextjs.md +21 -8
  101. package/docs/examples/scoped-agent.md +93 -0
  102. package/docs/examples/server-agent.md +27 -5
  103. package/docs/guarantees.md +29 -17
  104. package/docs/identity.md +198 -121
  105. package/docs/index.md +35 -18
  106. package/docs/integration-guide.md +79 -83
  107. package/docs/interaction-model.md +40 -25
  108. package/docs/mcp/claude-code.md +9 -17
  109. package/docs/mcp/cursor.md +6 -24
  110. package/docs/mcp/windsurf.md +6 -19
  111. package/docs/mcp.md +103 -26
  112. package/docs/quickstart.md +31 -39
  113. package/docs/react.md +18 -14
  114. package/docs/roadmap.md +15 -3
  115. package/docs/schema-contract.md +109 -0
  116. package/examples/README.md +8 -4
  117. package/examples/data-source/README.md +6 -2
  118. package/examples/data-source/run.ts +4 -3
  119. package/examples/quickstart.ts +1 -1
  120. package/llms.txt +27 -16
  121. package/package.json +13 -1
@@ -1,39 +1,26 @@
1
1
  # Integration Guide
2
2
 
3
- Use this guide when you are adding Ablo to a real product, not a demo.
3
+ If humans and AI agents both edit the same records in your app, they overwrite
4
+ each other and there's no good place to coordinate. Ablo gives them one shared,
5
+ typed write path — the same `ablo.<model>.update(...)` call for a React
6
+ component, a server action, a background worker, or an agent — and reconciles the
7
+ edits. This guide adds it to a product that already has a backend and database,
8
+ one model at a time.
4
9
 
5
- ## Why Ablo, before the API
6
-
7
- Ablo is a sync engine designed from the ground up for **humans and AI
8
- agents editing the same state at the same time**. That premise drives
9
- every design choice in this guide; if you only need server-to-server
10
- data syncing without agents in the loop, the trade-offs land elsewhere
11
- (Replicache, ElectricSQL, PowerSync are good answers for human-only
12
- real-time apps; Zero is a good answer for query-shaped sync).
13
-
14
- The shape of the SDK reflects three commitments:
10
+ Three things hold no matter which actor is writing:
15
11
 
16
12
  - **One model API for every actor.** `ablo.<model>.update(...)` is what
17
13
  React components, server actions, background workers, and AI agents
18
14
  all call. No separate "agent SDK," no parallel mutation path. The
19
15
  attribution comes from the credential, not the call site.
20
- - **Server owns the scope convention; client picks a subset by id.** The
21
- `org:` / `user:` / `team:` (or your own `region:` / `customer:`)
22
- prefixes live in the schema's `identityRoles` once, never typed by
23
- consumer code. Same boundary Liveblocks (`prepareSession`), PowerSync
24
- (named streams), and Zero (synced queries) settled on after the same
25
- realization: clients that compose scope strings drift; servers that
26
- derive scope from authed identity don't.
27
- - **Capabilities, not API keys, are how agents authenticate.** Static
28
- API keys protect server-to-server humans. Agents get per-run,
29
- per-scope, leased credentials with per-request signature verification
30
- and instant revocation. The 2025-2026 AI-agent auth consensus
31
- (OAuth 2.1 / MCP, AWS STS, Vault leases, Auth0 Token Vault) converged
32
- on this shape. Capabilities are Ablo's instance.
33
-
34
- If you have already built a sync layer or an agent runtime, you know
35
- what each of those costs. This guide assumes you want them solved once,
36
- together, behind one client.
16
+ - **You never type `org:123` in client code.** The server derives what each
17
+ caller can see from their authenticated identity, using the `identityRoles`
18
+ you declare once in the schema. The client just names which model and id it
19
+ wants. The `org:` / `user:` / `team:` (or your own `region:` / `customer:`)
20
+ prefixes live in the schema, never in consumer code.
21
+ - **Agents don't use your account API key.** Each agent run gets a short-lived
22
+ credential scoped to just what that run can touch, verified per request and
23
+ revocable instantly. (See the Agents section below for the actual calls.)
37
24
 
38
25
  ## The integration in one diagram
39
26
 
@@ -49,7 +36,7 @@ Declare the models Ablo coordinates, then read and write through
49
36
  that same model path.
50
37
 
51
38
  ```txt
52
- schema -> ablo.<model>.load(...) -> ablo.<model>.update(...)
39
+ schema -> ablo.<model>.list(...) -> ablo.<model>.update(...)
53
40
  ```
54
41
 
55
42
  Commits and receipts exist under the hood. Most apps do not create protocol
@@ -101,10 +88,10 @@ wait: 'confirmed' }), and add a smoke test for two concurrent writers.
101
88
 
102
89
  Start with fields and relations. Keep load strategies, indexing hints, and
103
90
  read-only/mutable shortcuts out of the first version unless you already need
104
- offline-heavy local cache behavior.
91
+ them.
105
92
 
106
93
  ```ts
107
- // src/ablo.schema.ts
94
+ // src/ablo/schema.ts
108
95
  import { defineSchema, model, z } from '@abloatai/ablo/schema';
109
96
 
110
97
  export const schema = defineSchema(
@@ -119,23 +106,15 @@ export const schema = defineSchema(
119
106
  }),
120
107
  },
121
108
  {
122
- // Identity-anchored sync-group roles. The server walks these to
123
- // build each participant's allowed subscription set from the
124
- // resolved identity context. Templates and extractors are fully
125
- // consumer-controlled no hardcoded `org:` / `user:` convention
126
- // anywhere in the engine. Omit `identityRoles` entirely if your
127
- // schema doesn't need identity-derived scoping.
109
+ // Identity-anchored sync-group roles. The server walks these to build each
110
+ // participant's allowed subscription set from the resolved identity context.
111
+ // `kind` is the group prefix; `source` is the identity field to read both
112
+ // consumer-controlled, no hardcoded `org:` / `user:` convention anywhere in
113
+ // the engine. Pure data (no closures), so the schema stays JSON-serializable.
114
+ // Omit `identityRoles` entirely if you don't need identity-derived scoping.
128
115
  identityRoles: [
129
- {
130
- kind: 'tenant',
131
- template: 'org:{id}',
132
- extract: (i) => (i.organizationId ? [String(i.organizationId)] : []),
133
- },
134
- {
135
- kind: 'participant',
136
- template: 'user:{id}',
137
- extract: (i) => (i.userId ? [String(i.userId)] : []),
138
- },
116
+ identityRole({ kind: 'org', source: 'organizationId' }),
117
+ identityRole({ kind: 'user', source: 'userId' }),
139
118
  ],
140
119
  }
141
120
  );
@@ -143,11 +122,15 @@ export const schema = defineSchema(
143
122
 
144
123
  ### Declaring scope on a model
145
124
 
146
- Per-row tenancy and per-entity sync-group anchors live on the
147
- `defineModel` (or `model(...)`) options. The two halves compose: the
148
- identity roles above produce a participant's _allowed_ set; the
149
- per-model options below define how rows are filtered server-side and
150
- which sync-group label each row fans out on.
125
+ > **Canonical reference: [Identity & Sync Groups](./identity.md).** This is the
126
+ > short version — `scope` (root), `parent` (containment), `grants` (membership),
127
+ > and the model-form `scope` prop are all covered in depth there. Read it once;
128
+ > this guide only shows the minimal shape inline.
129
+
130
+ Per-row tenancy and per-entity sync-group anchors live on the `model(...)`
131
+ options. The two halves compose: the identity roles above produce a
132
+ participant's _allowed_ set; the per-model options below define how rows are
133
+ filtered server-side and which sync-group each row fans out on.
151
134
 
152
135
  ```ts
153
136
  model(
@@ -159,9 +142,9 @@ model(
159
142
  // Rows carry organization_id and bootstrap filters on it.
160
143
  orgScoped: true,
161
144
 
162
- // Per-entity sync-group anchor. Lets a scoped session narrow into
163
- // one row's scope via `syncGroupFormat.replace('{id}', rowId)`.
164
- syncGroupFormat: 'matter:{id}',
145
+ // Scope root: rows form the group `matter:<id>`. Children point at it with
146
+ // `relation.belongsTo('matters', 'matterId', { parent: true })` to inherit.
147
+ scope: 'matter',
165
148
  }
166
149
  );
167
150
  ```
@@ -178,7 +161,7 @@ Trusted runtimes can use `ABLO_API_KEY`.
178
161
  ```ts
179
162
  // src/ablo.ts
180
163
  import Ablo from '@abloatai/ablo';
181
- import { schema } from './ablo.schema';
164
+ import { schema } from './ablo/schema';
182
165
 
183
166
  export const ablo = Ablo({
184
167
  schema,
@@ -194,7 +177,7 @@ server API key in the bundle.
194
177
  'use client';
195
178
 
196
179
  import { AbloProvider } from '@abloatai/ablo/react';
197
- import { schema } from '@/ablo.schema';
180
+ import { schema } from '@/ablo/schema';
198
181
 
199
182
  export function Providers({ children }: { children: React.ReactNode }) {
200
183
  return <AbloProvider schema={schema}>{children}</AbloProvider>;
@@ -230,21 +213,28 @@ refreshes before expiry.
230
213
 
231
214
  ## 3. Read State
232
215
 
233
- Use `load` when the row may not already be local.
216
+ Reads come in two flavors, and you pick based on whether you can wait.
217
+ `retrieve(id)` and `list({ where })` hit the server (and hydrate the local
218
+ store) — they're async, so you `await` them. `get(id)`, `getAll({ where })`,
219
+ and `getCount({ where })` read the already-synced local graph synchronously, so
220
+ they're the ones you call in render.
221
+
222
+ Use `retrieve` when the row may not be local yet — it fetches from the server
223
+ and waits.
234
224
 
235
225
  ```ts
236
226
  await ablo.ready();
237
227
 
238
- const [report] = await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
228
+ const report = await ablo.weatherReports.retrieve('report_stockholm');
239
229
  if (!report) throw new Error('report not found');
240
230
  ```
241
231
 
242
- Use `retrieve`, `list`, and `count` for synchronous local reads after data has
243
- loaded.
232
+ Use `get`, `getAll`, and `getCount` for synchronous local-graph reads after
233
+ data has synced.
244
234
 
245
235
  ```ts
246
- const report = ablo.weatherReports.retrieve('report_stockholm');
247
- const activeReports = ablo.weatherReports.list({
236
+ const report = ablo.weatherReports.get('report_stockholm');
237
+ const activeReports = ablo.weatherReports.getAll({
248
238
  where: { projectId: 'proj_123' },
249
239
  filter: (report) => report.status !== 'ready',
250
240
  orderBy: { updatedAt: 'desc' },
@@ -264,8 +254,8 @@ export function ReportRow({
264
254
  }: {
265
255
  report: { id: string; location: string; status: string };
266
256
  }) {
267
- const report = useAblo((ablo) => ablo.weatherReports.retrieve(serverReport.id)) ?? serverReport;
268
- const active = useAblo((ablo) => ablo.weatherReports.claimState(serverReport.id));
257
+ const report = useAblo((ablo) => ablo.weatherReports.get(serverReport.id)) ?? serverReport;
258
+ const active = useAblo((ablo) => ablo.weatherReports.claim.state(serverReport.id));
269
259
 
270
260
  return <button disabled={Boolean(active) || report.status === 'ready'}>{report.location}</button>;
271
261
  }
@@ -346,11 +336,11 @@ The migration can be gradual:
346
336
 
347
337
  1. Declare schema for one model, such as `reports`.
348
338
  2. Keep existing server loads for first paint.
349
- 3. Add `useAblo((ablo) => ablo.weatherReports.retrieve(id)) ?? serverReport` for live rows.
339
+ 3. Add `useAblo((ablo) => ablo.weatherReports.get(id)) ?? serverReport` for live rows.
350
340
  4. Add one Data Source endpoint that calls the existing service layer.
351
341
  5. Move one mutation button from `fetch('/api/reports/...')` to `ablo.weatherReports.update(...)`.
352
342
  6. Add an outbox/events path for writes that still happen outside Ablo.
353
- 7. Let agents use the same `ablo.weatherReports.load(...)` and `ablo.weatherReports.update(...)`.
343
+ 7. Let agents use the same `ablo.weatherReports.list(...)` and `ablo.weatherReports.update(...)`.
354
344
 
355
345
  For the full Python shape, see
356
346
  [Existing Python Backend](./examples/existing-python-backend.md).
@@ -362,7 +352,7 @@ Use a Data Source when your app database remains the source of truth.
362
352
  ```ts
363
353
  // app/api/ablo/source/route.ts
364
354
  import { dataSource } from '@abloatai/ablo';
365
- import { schema } from '@/ablo.schema';
355
+ import { schema } from '@/ablo/schema';
366
356
  import { db } from '@/db';
367
357
 
368
358
  export const POST = dataSource({
@@ -411,8 +401,13 @@ The API key verifies Ablo's request. It is not a database credential.
411
401
  Agents should use the same model methods as the app when they can import the
412
402
  schema.
413
403
 
404
+ An agent often reads a row, calls an LLM, then writes back — a slow gap during
405
+ which a human might touch the same row. Wrap that work in a claim. Claims don't
406
+ lock. If another writer holds the row, `claim` waits for them, re-reads the
407
+ fresh row, then hands it to you — so two writers serialize instead of clobbering.
408
+
414
409
  ```ts
415
- const [report] = await ablo.weatherReports.load({ where: { id: reportId } });
410
+ const report = await ablo.weatherReports.retrieve(reportId);
416
411
  if (!report) return;
417
412
 
418
413
  await ablo.weatherReports.claim(
@@ -458,10 +453,10 @@ Keep agent writes on the same schema client surface as the app.
458
453
  | `/react` | Live React selectors, provider lifecycle, presence, sync status. |
459
454
  | `/testing` | Test harnesses and deterministic mocks. |
460
455
  | `Data Source` | Keep your app database canonical. |
461
- | `persistence: 'indexeddb'` | Durable browser cache and offline queueing for apps that need it. |
462
- | `claim` / `claimState` / `queue` | Show active work and coordinate before a write. |
456
+ | `persistence: 'indexeddb'` | Durable browser cache that survives reloads, for apps that need it. |
457
+ | `claim` / `claim.state` / `claim.queue` | Show active work and coordinate before a write. |
463
458
  | `snapshot` + `readAt` | Reject writes based on stale state. |
464
- | `mutable`, `readOnly`, `field`, `indexed` | Advanced schema and local-cache tuning. |
459
+ | `mutable`, `readOnly`, `field`, `indexed` | Advanced schema and read tuning. |
465
460
 
466
461
  The first integration should not need most of these. Start with schema and
467
462
  model methods, then add the optional pieces where the product actually needs
@@ -469,16 +464,17 @@ them.
469
464
 
470
465
  ## Method Cheatsheet
471
466
 
472
- | Method | Use it for |
473
- | ---------------------------- | ---------------------------------------------------------------- |
474
- | `load({ where })` | Async hydration from backing store/server. |
475
- | `retrieve(id)` | Synchronous local read of one loaded row. |
476
- | `list(options?)` | Synchronous local collection read. |
477
- | `count(options?)` | Synchronous local count. |
478
- | `create(data, options?)` | Create through the model client. |
479
- | `update(id, data, options?)` | Update through the model client. |
480
- | `delete(id, options?)` | Delete through the model client. |
481
- | `claimState(id)` | See active work on a model row. |
482
- | `claim(id, work, options?)` | Wait for your turn, re-read, and hold the row while `work` runs. |
467
+ | Method | Use it for |
468
+ | ---------------------------- | --------------------------------------------------------------------------- |
469
+ | `retrieve(id)` | Async read of one row from the server (await it). |
470
+ | `list({ where })` | Async read of many rows from the server (await it). |
471
+ | `get(id)` | Synchronous local read of one synced row (use in render). |
472
+ | `getAll({ where })` | Synchronous local read of many synced rows. |
473
+ | `getCount({ where })` | Synchronous local count of synced rows. |
474
+ | `create(data, options?)` | Create through the model client. |
475
+ | `update(id, data, options?)` | Update through the model client. |
476
+ | `delete(id, options?)` | Delete through the model client. |
477
+ | `claim.state(id)` | See who is currently working on a row (synchronous). |
478
+ | `claim(id, work, options?)` | Wait for your turn, re-read, and hold the row while `work` runs. |
483
479
 
484
480
  Keep first integrations on the model methods above.
@@ -1,28 +1,40 @@
1
1
  # Interaction Model
2
2
 
3
- Ablo's public model is the path every human UI, server action, and agent uses on
4
- every write:
3
+ When a person, a server action, and an AI agent can all write to the same row,
4
+ you need one write path that stops them from clobbering each other. Ablo gives
5
+ you exactly one: load the row, claim it while you work, update it, and wait for
6
+ confirmation. This page walks through that path and the few primitives behind it.
5
7
 
8
+ Here's the whole path in one block — claim a row, update it inside the claim, and
9
+ let the claim release when your callback returns:
10
+
11
+ ```ts
12
+ const report = await ablo.weatherReports.retrieve('report_stockholm');
13
+
14
+ await ablo.weatherReports.claim('report_stockholm', async (report) => {
15
+ await ablo.weatherReports.update(report.id, { status: 'ready' }, { wait: 'confirmed' });
16
+ });
6
17
  ```
7
- Schema -> Model load -> Claim -> Model update -> Confirmation
8
- ```
18
+
19
+ Claims don't lock. If another writer holds the row, `claim` waits for them,
20
+ re-reads the fresh row, then hands it to you — so two writers serialize instead
21
+ of clobbering.
9
22
 
10
23
  ## Primitives
11
24
 
12
25
  | Primitive | Plane | Purpose |
13
26
  |---|---|---|
14
27
  | `Schema` | State | Declares typed models the app and agents can read and write. |
15
- | `Model` | State | The generated `ablo.<model>` model. Use `load`, `retrieve`, `create`, `update`, and `delete`. |
16
- | `Claim` | Coordination | Who is working on a target. Claimed via `ablo.<model>.claim(id, ...)` and read via `ablo.<model>.claimState(id)`. Ephemeral — never persisted. |
28
+ | `Model` | State | The generated `ablo.<model>` model. Use `retrieve`/`list` (async server reads), `get`/`getAll`/`getCount` (synchronous local reads), `create`, `update`, and `delete`. |
29
+ | `Claim` | Coordination | Who is working on a target. Taken via `ablo.<model>.claim(id, work)` and read via `ablo.<model>.claim.state(id)`. Ephemeral — never persisted. |
17
30
  | `Commit` | Protocol | The durable write underneath model updates. Most users do not call it directly. |
18
31
  | `Receipt` | Protocol | The lower-level durable result for custom runtimes. Schema writes use `wait: 'confirmed'`. |
19
32
 
20
33
  ### Why each primitive is separate
21
34
 
22
- The plane separation isn't ceremony collapsing any two of these would
23
- lose a property that's hard to recover later. A reader coming from
24
- Replicache or Yjs would expect just `Commit`; here's what the others buy
25
- you over that minimum:
35
+ Why are `Claim`, `Commit`, and `Receipt` separate things instead of one? Each
36
+ does a job the others can't. If you're coming from Replicache or Yjs you'd
37
+ expect just `Commit`; here's what the other two buy you over that minimum:
26
38
 
27
39
  - **`Claim` is not a read lock.** Reads stay open. Claims serialize
28
40
  acting-on-the-row, so slow work can wait in FIFO order, re-read, and write
@@ -33,42 +45,45 @@ you over that minimum:
33
45
  client. A status code can't be re-read by a sub-agent that wasn't on
34
46
  the original call.
35
47
 
36
- The shape is borrowed from systems that learned the cost of collapse:
37
- coordination from operational-transform CRDTs and Linear's optimistic
38
- multiplayer model, and receipts from durable write protocols.
39
-
40
48
  ## Run Loop
41
49
 
42
50
  A normal schema-backed run is:
43
51
 
44
- ```
45
- const [report] = await ablo.weatherReports.load({ where: { id } });
46
- const active = ablo.weatherReports.claimState(id);
52
+ ```ts
53
+ const report = await ablo.weatherReports.retrieve(id);
54
+ const active = ablo.weatherReports.claim.state(id);
47
55
  await ablo.weatherReports.claim(id, async (report) => {
48
56
  await ablo.weatherReports.update(report.id, patch, { wait: 'confirmed' });
49
57
  });
50
58
  ```
51
59
 
60
+ `retrieve(id)` is an async server read (await it). `claim.state(id)` is a
61
+ synchronous local read of who currently holds the row — it never blocks.
62
+
52
63
  ## Coordination
53
64
 
54
- Claims broadcast across the org. Claim a row through the flat model verb, write
55
- through the normal `update`, and the claim releases when the callback returns:
65
+ > Loop view only. Full claim reference methods, the claim-state object, the
66
+ > `claim.queue`, errors is [Coordination](./coordination.md).
67
+
68
+ Claims broadcast across the org. Call `claim(id, callback)`, do your writes with
69
+ the normal `update` inside the callback, and the claim releases automatically
70
+ when the callback returns:
56
71
 
57
72
  ```ts
58
73
  await ablo.weatherReports.claim(
59
74
  'report_stockholm',
60
75
  async (report) => {
61
- await ablo.weatherReports.update(report.id, { status: 'ready' }); // stale-guarded under the claim
76
+ await ablo.weatherReports.update(report.id, { status: 'ready' }); // rejected if the row changed under the claim
62
77
  },
63
78
  { action: 'editing' },
64
79
  );
65
80
  ```
66
81
 
67
- `ablo.weatherReports.claimState('report_stockholm')` reads the live claim (or `null`) without
68
- blocking. The claim is **advisory**: if another participant holds the row,
69
- `claim` waits for them to finish and re-reads before handing back the row. The
70
- same signal is visible to every schema client through `claimState(id)` and the live
71
- claim stream.
82
+ `ablo.weatherReports.claim.state('report_stockholm')` reads the live claim (or
83
+ `null`) without blocking. Claims don't lock: if another participant holds the
84
+ row, `claim` waits for them to finish, re-reads, and then hands you the fresh
85
+ row. The same signal is visible to every schema client through `claim.state(id)`
86
+ and the live claim stream.
72
87
 
73
88
  ## Conflict resolution
74
89
 
@@ -6,19 +6,9 @@
6
6
  claude mcp add --transport http ablo-sync https://<your-app>/api/mcp
7
7
  ```
8
8
 
9
- That's it. The next `/help` in Claude Code will list the Ablo Sync tools.
10
-
11
- ## With auth
12
-
13
- If your deployment requires a scoped bearer token (production setups should):
14
-
15
- ```bash
16
- claude mcp add --transport http ablo-sync https://<your-app>/api/mcp \
17
- --header "Authorization=Bearer $ABLO_MCP_TOKEN"
18
- ```
19
-
20
- Create a session-scoped bearer token from your server or dashboard — see
21
- [MCP overview](/docs/mcp#auth).
9
+ That's it no token or header needed. The endpoint is public and serves
10
+ only docs, schema lint, and scaffolds. The next `/help` in Claude Code will
11
+ list the Ablo Sync tools.
22
12
 
23
13
  ## Verify
24
14
 
@@ -28,7 +18,9 @@ In Claude Code, run:
28
18
  /mcp list
29
19
  ```
30
20
 
31
- You should see `ablo-sync` with the model tools enumerated.
21
+ You should see `ablo-sync` with the integration tools enumerated:
22
+ `search_ablo_docs`, `get_recipe`, `get_api_surface`, `validate_schema`,
23
+ `scaffold_app`.
32
24
 
33
25
  ## Removing
34
26
 
@@ -38,6 +30,6 @@ claude mcp remove ablo-sync
38
30
 
39
31
  ## More
40
32
 
41
- - [MCP overview](/docs/mcp) — how the transport works.
42
- - [Cursor setup](/docs/mcp/cursor) — same JSON, different UI.
43
- - [Windsurf setup](/docs/mcp/windsurf) — same JSON, different UI.
33
+ - [MCP overview](/docs/mcp) — what the server exposes and how the transport works.
34
+ - [Cursor setup](/docs/mcp/cursor) — same URL, different UI.
35
+ - [Windsurf setup](/docs/mcp/windsurf) — same URL, different UI.
@@ -15,39 +15,21 @@ Add the Ablo Sync MCP server to Cursor's `mcp.json`:
15
15
  }
16
16
  ```
17
17
 
18
- The file lives at `~/.cursor/mcp.json` on macOS / Linux.
18
+ The file lives at `~/.cursor/mcp.json` on macOS / Linux. No auth header is
19
+ needed — the endpoint is public and serves only docs, schema lint, and
20
+ scaffolds.
19
21
 
20
22
  Restart Cursor. The Ablo Sync tools appear under the MCP icon in the agent
21
23
  panel.
22
24
 
23
- ## With auth
24
-
25
- Add a `headers` block:
26
-
27
- ```json
28
- {
29
- "mcpServers": {
30
- "ablo-sync": {
31
- "transport": "http",
32
- "url": "https://<your-app>/api/mcp",
33
- "headers": {
34
- "Authorization": "Bearer $ABLO_MCP_TOKEN"
35
- }
36
- }
37
- }
38
- }
39
- ```
40
-
41
- Cursor expands shell-style env vars in this block. Set `ABLO_MCP_TOKEN`
42
- in your shell config.
43
-
44
25
  ## Verify
45
26
 
46
27
  In Cursor's agent panel, open the MCP tools list. You should see the
47
- Ablo Sync model tools and their JSON schemas.
28
+ Ablo Sync integration tools and their JSON schemas: `search_ablo_docs`,
29
+ `get_recipe`, `get_api_surface`, `validate_schema`, `scaffold_app`.
48
30
 
49
31
  ## More
50
32
 
51
- - [MCP overview](/docs/mcp) — how the transport works.
33
+ - [MCP overview](/docs/mcp) — what the server exposes and how the transport works.
52
34
  - [Claude Code setup](/docs/mcp/claude-code) — CLI install.
53
35
  - [Windsurf setup](/docs/mcp/windsurf) — same JSON shape.
@@ -16,31 +16,18 @@ Add the Ablo Sync MCP server to Windsurf's MCP config:
16
16
  ```
17
17
 
18
18
  The config path differs by platform — Windsurf surfaces it in Settings →
19
- Cascade → MCP. Restart Windsurf after saving.
20
-
21
- ## With auth
22
-
23
- ```json
24
- {
25
- "mcpServers": {
26
- "ablo-sync": {
27
- "transport": "http",
28
- "url": "https://<your-app>/api/mcp",
29
- "headers": {
30
- "Authorization": "Bearer $ABLO_MCP_TOKEN"
31
- }
32
- }
33
- }
34
- }
35
- ```
19
+ Cascade → MCP. Restart Windsurf after saving. No auth header is needed —
20
+ the endpoint is public and serves only docs, schema lint, and scaffolds.
36
21
 
37
22
  ## Verify
38
23
 
39
24
  Cascade's MCP panel lists every configured server with its tools. You
40
- should see `ablo-sync` with model tools enumerated.
25
+ should see `ablo-sync` with the integration tools enumerated:
26
+ `search_ablo_docs`, `get_recipe`, `get_api_surface`, `validate_schema`,
27
+ `scaffold_app`.
41
28
 
42
29
  ## More
43
30
 
44
- - [MCP overview](/docs/mcp) — how the transport works.
31
+ - [MCP overview](/docs/mcp) — what the server exposes and how the transport works.
45
32
  - [Claude Code setup](/docs/mcp/claude-code) — CLI install.
46
33
  - [Cursor setup](/docs/mcp/cursor) — same JSON shape.