@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.
- package/CHANGELOG.md +77 -0
- package/README.md +95 -57
- package/dist/BaseSyncedStore.d.ts +1 -1
- package/dist/BaseSyncedStore.js +8 -4
- package/dist/SyncEngineContext.d.ts +2 -1
- package/dist/SyncEngineContext.js +5 -3
- package/dist/agent/session.js +3 -2
- package/dist/auth/index.js +39 -11
- package/dist/client/Ablo.d.ts +112 -3
- package/dist/client/Ablo.js +144 -10
- package/dist/client/ApiClient.d.ts +32 -0
- package/dist/client/ApiClient.js +76 -44
- package/dist/client/auth.d.ts +11 -1
- package/dist/client/auth.js +21 -2
- package/dist/client/createModelProxy.d.ts +120 -53
- package/dist/client/createModelProxy.js +66 -31
- package/dist/client/identity.js +14 -0
- package/dist/client/registerDataSource.d.ts +19 -0
- package/dist/client/registerDataSource.js +57 -0
- package/dist/client/validateAbloOptions.d.ts +2 -1
- package/dist/client/validateAbloOptions.js +8 -7
- package/dist/coordination/index.d.ts +6 -0
- package/dist/coordination/index.js +6 -0
- package/dist/coordination/schema.d.ts +329 -0
- package/dist/coordination/schema.js +209 -0
- package/dist/core/QueryView.d.ts +4 -1
- package/dist/core/QueryView.js +1 -1
- package/dist/core/query-utils.d.ts +7 -10
- package/dist/core/query-utils.js +2 -3
- package/dist/errorCodes.d.ts +286 -0
- package/dist/errorCodes.js +284 -0
- package/dist/errors.d.ts +103 -7
- package/dist/errors.js +192 -41
- package/dist/index.d.ts +11 -6
- package/dist/index.js +10 -6
- package/dist/keys/index.d.ts +61 -0
- package/dist/keys/index.js +151 -0
- package/dist/policy/index.d.ts +1 -1
- package/dist/policy/index.js +1 -1
- package/dist/policy/types.d.ts +31 -0
- package/dist/policy/types.js +15 -0
- package/dist/query/client.js +19 -8
- package/dist/react/AbloProvider.d.ts +37 -0
- package/dist/react/AbloProvider.js +107 -4
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/DefaultFallback.d.ts +1 -1
- package/dist/react/SyncGroupProvider.d.ts +1 -1
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +3 -2
- package/dist/react/useAblo.d.ts +4 -4
- package/dist/react/useAblo.js +10 -5
- package/dist/react/useReactive.js +16 -3
- package/dist/schema/ddl.d.ts +62 -0
- package/dist/schema/ddl.js +317 -0
- package/dist/schema/diff.d.ts +6 -0
- package/dist/schema/diff.js +21 -3
- package/dist/schema/field.d.ts +16 -19
- package/dist/schema/field.js +30 -17
- package/dist/schema/index.d.ts +7 -4
- package/dist/schema/index.js +9 -3
- package/dist/schema/model.d.ts +87 -25
- package/dist/schema/model.js +33 -3
- package/dist/schema/relation.d.ts +17 -0
- package/dist/schema/roles.d.ts +148 -0
- package/dist/schema/roles.js +149 -0
- package/dist/schema/schema.d.ts +2 -112
- package/dist/schema/schema.js +50 -62
- package/dist/schema/select.d.ts +25 -0
- package/dist/schema/select.js +55 -0
- package/dist/schema/serialize.d.ts +16 -12
- package/dist/schema/serialize.js +16 -12
- package/dist/schema/sugar.d.ts +20 -3
- package/dist/schema/sugar.js +5 -1
- package/dist/schema/tenancy.d.ts +66 -0
- package/dist/schema/tenancy.js +58 -0
- package/dist/sync/BootstrapHelper.js +46 -27
- package/dist/sync/ConnectionManager.d.ts +3 -1
- package/dist/sync/ConnectionManager.js +37 -1
- package/dist/sync/HydrationCoordinator.d.ts +2 -0
- package/dist/sync/HydrationCoordinator.js +26 -19
- package/dist/sync/NetworkProbe.d.ts +8 -0
- package/dist/sync/NetworkProbe.js +24 -2
- package/dist/sync/SyncWebSocket.d.ts +1 -1
- package/dist/sync/SyncWebSocket.js +43 -53
- package/dist/sync/createIntentStream.d.ts +2 -1
- package/dist/sync/createIntentStream.js +46 -1
- package/dist/sync/participants.js +10 -16
- package/dist/transactions/TransactionQueue.js +13 -1
- package/dist/types/streams.d.ts +53 -33
- package/docs/api-keys.md +47 -3
- package/docs/api.md +103 -57
- package/docs/audit.md +16 -9
- package/docs/cli.md +222 -0
- package/docs/client-behavior.md +35 -21
- package/docs/coordination.md +74 -36
- package/docs/data-sources.md +23 -21
- package/docs/examples/agent-human.md +72 -28
- package/docs/examples/ai-sdk-tool.md +14 -11
- package/docs/examples/existing-python-backend.md +30 -19
- package/docs/examples/nextjs.md +21 -8
- package/docs/examples/scoped-agent.md +93 -0
- package/docs/examples/server-agent.md +27 -5
- package/docs/guarantees.md +29 -17
- package/docs/identity.md +198 -121
- package/docs/index.md +35 -18
- package/docs/integration-guide.md +79 -83
- package/docs/interaction-model.md +40 -25
- package/docs/mcp/claude-code.md +9 -17
- package/docs/mcp/cursor.md +6 -24
- package/docs/mcp/windsurf.md +6 -19
- package/docs/mcp.md +103 -26
- package/docs/quickstart.md +31 -39
- package/docs/react.md +18 -14
- package/docs/roadmap.md +15 -3
- package/docs/schema-contract.md +109 -0
- package/examples/README.md +8 -4
- package/examples/data-source/README.md +6 -2
- package/examples/data-source/run.ts +4 -3
- package/examples/quickstart.ts +1 -1
- package/llms.txt +27 -16
- package/package.json +13 -1
|
@@ -1,39 +1,26 @@
|
|
|
1
1
|
# Integration Guide
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
- **
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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>.
|
|
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
|
-
|
|
91
|
+
them.
|
|
105
92
|
|
|
106
93
|
```ts
|
|
107
|
-
// src/ablo
|
|
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
|
-
//
|
|
124
|
-
//
|
|
125
|
-
// consumer-controlled
|
|
126
|
-
//
|
|
127
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
`
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 `
|
|
243
|
-
|
|
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.
|
|
247
|
-
const activeReports = ablo.weatherReports.
|
|
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.
|
|
268
|
-
const active = useAblo((ablo) => ablo.weatherReports.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
462
|
-
| `claim` / `
|
|
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
|
|
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
|
-
| `
|
|
475
|
-
| `
|
|
476
|
-
| `
|
|
477
|
-
| `
|
|
478
|
-
| `
|
|
479
|
-
| `
|
|
480
|
-
| `
|
|
481
|
-
| `
|
|
482
|
-
| `claim(id
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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 `
|
|
16
|
-
| `Claim` | Coordination | Who is working on a target.
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
46
|
-
const active = ablo.weatherReports.
|
|
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
|
-
|
|
55
|
-
|
|
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' }); //
|
|
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.
|
|
68
|
-
blocking.
|
|
69
|
-
`claim` waits for them to finish
|
|
70
|
-
same signal is visible to every schema client through `
|
|
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
|
|
package/docs/mcp/claude-code.md
CHANGED
|
@@ -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
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
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
|
|
43
|
-
- [Windsurf setup](/docs/mcp/windsurf) — same
|
|
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.
|
package/docs/mcp/cursor.md
CHANGED
|
@@ -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
|
|
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.
|
package/docs/mcp/windsurf.md
CHANGED
|
@@ -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
|
|
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.
|