@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
@@ -1,17 +1,10 @@
1
1
  # Interaction Model
2
2
 
3
- Ablo separates the data path from the authority path.
4
-
5
- The data path is what your application does on every write:
6
-
7
- ```
8
- Schema -> Model load -> Intent -> Model update -> Confirmation
9
- ```
10
-
11
- The authority path is what makes that write defensible:
3
+ Ablo's public model is the path every human UI, server action, and agent uses on
4
+ every write:
12
5
 
13
6
  ```
14
- Capability -> Task -> Usage
7
+ Schema -> Model load -> Claim -> Model update -> Confirmation
15
8
  ```
16
9
 
17
10
  ## Primitives
@@ -19,16 +12,10 @@ Capability -> Task -> Usage
19
12
  | Primitive | Plane | Purpose |
20
13
  |---|---|---|
21
14
  | `Schema` | State | Declares typed models the app and agents can read and write. |
22
- | `Model` | State | The generated `ablo.<model>` resource. Use `load`, `retrieve`, `create`, `update`, and `delete`. |
23
- | `Intent` | Coordination | Who is working on a target, as one Stripe-shaped object with a single `status`. Opened and read through `ablo.<model>.intent(id)`. Ephemeral — never persisted. |
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. |
24
17
  | `Commit` | Protocol | The durable write underneath model updates. Most users do not call it directly. |
25
18
  | `Receipt` | Protocol | The lower-level durable result for custom runtimes. Schema writes use `wait: 'confirmed'`. |
26
- | `Capability` | Control | Signed credentials. It says who can do what, where, for how long, and on whose behalf. |
27
- | `Task` | Control | One agent run. It groups prompts, commits, child tasks, and cost. |
28
- | `Usage` | Control | Metering and audit rows derived from accepted work. |
29
-
30
- Capabilities, tasks, and usage do not mutate product data. They define and
31
- record the authority around mutation.
32
19
 
33
20
  ### Why each primitive is separate
34
21
 
@@ -37,94 +24,54 @@ lose a property that's hard to recover later. A reader coming from
37
24
  Replicache or Yjs would expect just `Commit`; here's what the others buy
38
25
  you over that minimum:
39
26
 
40
- - **`Intent` is not a lock.** A pessimistic lock blocks a writer; an
41
- intent *announces* one. Other writers can yield, wait, or proceed —
42
- the choice is theirs, not the system's. This is the only primitive
43
- that lets two agents discover each other's planned work *before* the
44
- conflict and self-arbitrate. Without intents, agents only learn about
45
- contention at commit time, when one of them has already wasted a
46
- token budget.
27
+ - **`Claim` is not a read lock.** Reads stay open. Claims serialize
28
+ acting-on-the-row, so slow work can wait in FIFO order, re-read, and write
29
+ from fresh state.
47
30
  - **`Receipt` is not a `200 OK`.** It's the durable artifact a commit
48
31
  produced — accepted commit id, server-assigned timestamps, stale-check
49
32
  outcome — addressable after the fact and replayable into a different
50
33
  client. A status code can't be re-read by a sub-agent that wasn't on
51
34
  the original call.
52
- - **`Capability` is not the actor.** The actor (`Task`) is what *ran*;
53
- the capability is what it was *allowed* to do. Same human can spawn
54
- many tasks under one cap (cheap re-run); same task can attenuate to
55
- many sub-caps (sub-agent delegation). Folding them collapses both
56
- directions of that fan.
57
- - **`Task` is not the credential.** It's the audit envelope: prompt,
58
- commits, child tasks, tokens, duration. Long after the cap has
59
- expired, the task row is what answers "what did this run do." Folding
60
- task into capability loses the post-expiry audit.
61
- - **`Usage` is not derived from logs.** It's denormalized at commit
62
- accept time so quota enforcement and billing reads stay O(1). Log
63
- scans would work for audit but not for hot-path gating.
64
35
 
65
36
  The shape is borrowed from systems that learned the cost of collapse:
66
- intents from operational-transform CRDTs and Linear's
67
- optimistic-multiplayer model, capabilities + tasks from AWS IAM
68
- (`Role` ≠ `RoleSession`) and Vault (`policy` ≠ `lease`).
37
+ coordination from operational-transform CRDTs and Linear's optimistic
38
+ multiplayer model, and receipts from durable write protocols.
69
39
 
70
40
  ## Run Loop
71
41
 
72
42
  A normal schema-backed run is:
73
43
 
74
44
  ```
75
- const [task] = await ablo.tasks.load({ where: { id } });
76
- const busy = ablo.intents.list({ resource: 'tasks', id });
77
- const snap = ablo.snapshot({ tasks: id });
78
- await ablo.tasks.update(id, patch, {
79
- readAt: snap.stamp,
80
- onStale: 'reject',
81
- wait: 'confirmed',
45
+ const [report] = await ablo.weatherReports.load({ where: { id } });
46
+ const active = ablo.weatherReports.claimState(id);
47
+ await ablo.weatherReports.claim(id, async (report) => {
48
+ await ablo.weatherReports.update(report.id, patch, { wait: 'confirmed' });
82
49
  });
83
50
  ```
84
51
 
85
- ## Participants
86
-
87
- Every action is performed by one of three kinds:
88
-
89
- - `user` — a human, authenticated via session.
90
- - `agent` — an AI process acting on behalf of a human, authenticated via a capability minted from that human's session.
91
- - `system` — a customer-backend process acting on behalf of an organization, authenticated via an API key.
92
-
93
- The participant kind is enforced at the boundary. An agent capability cannot impersonate a user. A user session cannot open a task.
94
-
95
- ## Delegation chain
96
-
97
- Every capability resolves to a `delegationChainRootUserId` — the human at the head of the chain. The chain is denormalized onto every commit's `on_behalf_of_*` columns so audit queries answer "what did this human authorize" with one lookup, not a recursive join.
98
-
99
- ## Enforcement
100
-
101
- Capabilities are enforced per operation, not per request. When a commit arrives, Ablo decodes the bearer token, checks each operation against `operations` and `syncGroups`, and rejects with `capability_scope_denied` if the scope is missing. Revocation takes effect within seconds of `DELETE /v1/capabilities/:id`.
102
-
103
- Three independent checks gate every commit. The redundancy is intentional — each check covers a failure mode the others don't:
104
-
105
- - **Lease (TTL on the token).** Decoded from the bearer; no DB lookup. Caps the lifetime of a leaked token. Without this, a stolen token works until manually revoked.
106
- - **Signature + scope verification.** Stateless. Detects forged or tampered tokens and rejects operations outside the cap's `operations` / `syncGroups`. Without this, a malformed token with the right shape could pass.
107
- - **Revocation.** `DELETE /v1/capabilities/:id` flips status server-side; live WS sessions close, future commits reject. Closes the gap between lease refresh cycles when you need *immediate* cutoff. Without this, a compromised cap with a long lease leaks until expiry.
108
-
109
- Removing any one of the three leaves a class of attack uncovered. The pattern matches AWS STS, Vault leases, and the OAuth 2.1 / MCP agent-auth recommendation; see [Capabilities](./capabilities.md#the-three-layer-security-model) for the full design discussion.
110
-
111
52
  ## Coordination
112
53
 
113
- Intents broadcast across the org. Open and read one on any row through the
114
- model accessor:
54
+ > Loop view only. Full claim reference methods, the claim-state object, the
55
+ > `queue`, errors — is [Coordination](./coordination.md).
56
+
57
+ Claims broadcast across the org. Claim a row through the flat model verb, write
58
+ through the normal `update`, and the claim releases when the callback returns:
115
59
 
116
60
  ```ts
117
- const task = ablo.tasks.intent('task_123');
118
- if (task.current) await task.whenFree(); // someone's working — wait
119
- await task.claim({ action: 'editing' }); // claim so others yield
120
- await task.update({ status: 'done' }); // commits + releases
61
+ await ablo.weatherReports.claim(
62
+ 'report_stockholm',
63
+ async (report) => {
64
+ await ablo.weatherReports.update(report.id, { status: 'ready' }); // stale-guarded under the claim
65
+ },
66
+ { action: 'editing' },
67
+ );
121
68
  ```
122
69
 
123
- `task.current` is the live `Intent` (or `null`); `task.whenFree()` resolves when
124
- the holder finishes. The same signal is visible to every schema client through
125
- `ablo.intents.list(...)` and the live intent stream, so callers decide whether
126
- to yield, wait, or fail fast. See [API Intent](./api.md#intent) for the object
127
- reference and lifecycle.
70
+ `ablo.weatherReports.claimState('report_stockholm')` reads the live claim (or `null`) without
71
+ blocking. The claim is **advisory**: if another participant holds the row,
72
+ `claim` waits for them to finish and re-reads before handing back the row. The
73
+ same signal is visible to every schema client through `claimState(id)` and the live
74
+ claim stream.
128
75
 
129
76
  ## Conflict resolution
130
77
 
@@ -137,16 +84,6 @@ Schema updates can carry `readAt` and `onStale`. If the state advanced past
137
84
 
138
85
  The choice is per-commit. No CRDT default; the policy is explicit.
139
86
 
140
- ## Audit
141
-
142
- Three tables observe the run:
143
-
144
- - `agent_tasks` — one row per open/close cycle. Cost stats, prompt hash, capability id.
145
- - `agent_actions_log` — one row per write, attributed to the task and the capability.
146
- - `usage_event` — one row per accounted API call, attributed to the api key, the participant, and the task.
147
-
148
- Joins between them answer "what did this agent do, on whose authority, at what cost." That answer is what makes giving an agent write access defensible.
149
-
150
87
  ## The contract in one sentence
151
88
 
152
- Declare schema, load state, coordinate intent, update the model, and wait for confirmation.
89
+ Declare schema, load state, coordinate a claim, update the model, and wait for confirmation.
@@ -10,14 +10,14 @@ That's it. The next `/help` in Claude Code will list the Ablo Sync tools.
10
10
 
11
11
  ## With auth
12
12
 
13
- If your deployment requires a capability token (production setups should):
13
+ If your deployment requires a scoped bearer token (production setups should):
14
14
 
15
15
  ```bash
16
16
  claude mcp add --transport http ablo-sync https://<your-app>/api/mcp \
17
17
  --header "Authorization=Bearer $ABLO_MCP_TOKEN"
18
18
  ```
19
19
 
20
- Create a session-scoped capability from the dashboard or via the API — see
20
+ Create a session-scoped bearer token from your server or dashboard — see
21
21
  [MCP overview](/docs/mcp#auth).
22
22
 
23
23
  ## Verify
@@ -28,7 +28,7 @@ In Claude Code, run:
28
28
  /mcp list
29
29
  ```
30
30
 
31
- You should see `ablo-sync` with the resource tools enumerated.
31
+ You should see `ablo-sync` with the model tools enumerated.
32
32
 
33
33
  ## Removing
34
34
 
@@ -44,7 +44,7 @@ in your shell config.
44
44
  ## Verify
45
45
 
46
46
  In Cursor's agent panel, open the MCP tools list. You should see the
47
- Ablo Sync resource tools and their JSON schemas.
47
+ Ablo Sync model tools and their JSON schemas.
48
48
 
49
49
  ## More
50
50
 
@@ -37,7 +37,7 @@ Cascade → MCP. Restart Windsurf after saving.
37
37
  ## Verify
38
38
 
39
39
  Cascade's MCP panel lists every configured server with its tools. You
40
- should see `ablo-sync` with resource tools enumerated.
40
+ should see `ablo-sync` with model tools enumerated.
41
41
 
42
42
  ## More
43
43
 
package/docs/mcp.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Model Context Protocol
2
2
 
3
- Ablo Sync ships an MCP server at `/api/mcp`. Connect any MCP-compatible AI
4
- assistant — Claude Code, Cursor, Windsurf — and your sync resources become
3
+ Ablo ships an MCP server at `/api/mcp`. Connect any MCP-compatible AI
4
+ assistant — Claude Code, Cursor, Windsurf — and your sync models become
5
5
  typed, callable tools.
6
6
 
7
7
  ## Install
@@ -14,38 +14,23 @@ Pick your client:
14
14
 
15
15
  ## How it works
16
16
 
17
- Each resource you declare becomes one or more MCP tools:
17
+ Each model you declare becomes one or more MCP tools:
18
18
 
19
- | Resource method | MCP tool name | What it does |
19
+ | Model method | MCP tool name | What it does |
20
20
  |---|---|---|
21
- | `retrieve` | `<resource>.retrieve` | Returns the row + a stamp. |
22
- | `list` | `<resource>.list` | Cursor-paginated discovery. |
23
- | `update` | `<resource>.update` | Write, requires the prior stamp. |
24
- | `<model>.intent` | `intent.create` | Claim a row before writing, then auto-release on update. |
21
+ | `retrieve` | `<model>.retrieve` | Returns the row + a stamp. |
22
+ | `list` | `<model>.list` | Cursor-paginated discovery. |
23
+ | `update` | `<model>.update` | Write, requires the prior stamp. |
24
+ | `<model>.claim` | `claim.create` | Claim a row before writing, then release when held work finishes. |
25
25
 
26
26
  The assistant gets typed JSON schemas, real argument types, and typed
27
27
  rejections when it writes stale state. No invention, no hallucinated IDs.
28
28
 
29
29
  ## Auth
30
30
 
31
- The MCP transport requires a capability token. Create one scoped to the
32
- assistant's session:
33
-
34
- ```ts
35
- const capability = await ablo.capabilities.create({
36
- participantKind: 'agent',
37
- participantId: 'agent:claude-code',
38
- // Strings derive from the schema's `identityRoles` templates
39
- // (see integration-guide.md §1).
40
- syncGroups: ['org:acme'],
41
- operations: ['tasks.retrieve', 'tasks.update'],
42
- label: 'claude-code dev session',
43
- lease: '8h',
44
- });
45
- ```
46
-
47
- Pass `capability.token` into the MCP client's auth header configuration.
48
- See your client's setup guide for the exact mechanism.
31
+ The MCP transport uses a scoped bearer token issued by your server. Pass that
32
+ token into the MCP client's auth header configuration. See your client's setup
33
+ guide for the exact mechanism.
49
34
 
50
35
  ## Limits
51
36
 
@@ -21,7 +21,7 @@ export ABLO_API_KEY=sk_test_...
21
21
  ```
22
22
 
23
23
  `ABLO_API_KEY` is for trusted server runtimes. Browser apps should use the React
24
- provider with a scoped capability/session route, not a bundled API key.
24
+ provider with a scoped session route, not a bundled API key.
25
25
 
26
26
  ## 3. Declare a Schema
27
27
 
@@ -44,7 +44,7 @@ export const ablo = Ablo({
44
44
  ```
45
45
 
46
46
  Customer apps should always pass `schema`. Treat it like Prisma's schema file:
47
- it is the source of truth for typed model resources, realtime subscriptions,
47
+ it is the source of truth for typed model clients, realtime subscriptions,
48
48
  agent writes, and Data Source requests.
49
49
 
50
50
  ## 4. Create and Update
@@ -81,71 +81,65 @@ ABLO_API_KEY=sk_test_... npx tsx quickstart.ts
81
81
  ## 6. AI Activity on Existing State
82
82
 
83
83
  When AI or background work will touch an existing row for more than a quick
84
- write, coordinate through `ablo.<model>.intent(id)`. It returns a handle
85
- synchronously read `.current` to see who's working on the row, `claim()` to
86
- claim it, `update()` to write under the claim (which auto-releases).
84
+ write, coordinate through the flat model verbs: `ablo.<model>.claim(id, ...)` to
85
+ claim the row (returns the row), `ablo.<model>.claimState(id)` to read who's working
86
+ on it (synchronous; never blocks), and the normal `ablo.<model>.update(id, ...)`
87
+ to write. Normal reads still work while the claim is held; server reads can opt
88
+ into `ifClaimed: 'wait'` or `ifClaimed: 'fail'` when they should not read through
89
+ active work. The callback form releases the claim when the callback returns or
90
+ throws.
87
91
 
88
92
  ```ts
89
- const report = ablo.weatherReports.intent('weather_stockholm');
90
-
91
- // If another participant holds it, wait for them to finish.
92
- if (report.current) await report.whenFree();
93
-
94
- // Claim it so other participants yield while we work.
95
- await report.claim({ action: 'checking_weather', field: 'forecast', ttl: '2m' });
96
-
97
- // Your existing weather tool or agent call. While this runs, other clients see
98
- // that weather_stockholm is being checked.
99
- const row = ablo.weatherReports.retrieve('weather_stockholm');
100
- const weather = await weatherAgent.getWeather(row.location);
101
-
102
- await report.update({
103
- status: 'ready',
104
- forecast: weather.summary,
105
- });
93
+ // Claim the row so other participants serialize behind us while we work.
94
+ await ablo.weatherReports.claim(
95
+ 'weather_stockholm',
96
+ async (report) => {
97
+ // Your existing weather tool or agent call. While this runs, other clients
98
+ // see that weather_stockholm is being checked.
99
+ const weather = await weatherAgent.getWeather(report.location);
100
+
101
+ await ablo.weatherReports.update(report.id, {
102
+ status: 'ready',
103
+ forecast: weather.summary,
104
+ });
105
+ },
106
+ { action: 'checking_weather', ttl: '2m' },
107
+ );
106
108
  ```
107
109
 
108
- Ablo does not fetch the weather. It keeps the activity visible while the work
109
- runs, rejects `report.update(...)` with `AbloStaleContextError` if the row
110
- changed under you, and releases the intent automatically once the write lands.
110
+ Ablo does not fetch the weather. The claim is **advisory**: if another
111
+ participant already holds the row, `claim` waits for them to finish and re-reads
112
+ before handing back the row. While you hold the claim, `update(id, ...)` is
113
+ stale-guarded and rejects with `AbloStaleContextError` if the row changed under
114
+ you. The claim releases automatically once the callback returns or throws.
111
115
 
112
- ## 7. Multiplayer and Busy Work
116
+ ## 7. Multiplayer and Claimed Work
113
117
 
114
118
  There is no separate multiplayer mode. Use the same schema client for human UI,
115
119
  server actions, and agents; Ablo fans out confirmed writes and keeps active
116
- intents visible on the same resource.
120
+ claims visible on the same model row.
117
121
 
118
- Intents tell you when another human or agent is active on the same target. For
119
- schema clients, wait on the intent stream and then write through the model.
122
+ `claimState(id)` tells you when another human or agent is active on the same row.
123
+ For schema clients, `claim(id, work)` waits fairly, re-reads, and then lets you
124
+ write through the model.
120
125
 
121
126
  ```ts
122
- const busy = ablo.intents.list({
123
- resource: 'weatherReports',
124
- id: 'weather_stockholm',
125
- });
126
-
127
- if (busy.length > 0) {
128
- await ablo.intents.waitFor(
129
- { resource: 'weatherReports', id: 'weather_stockholm' },
130
- { timeout: 30_000 },
131
- );
127
+ const active = ablo.weatherReports.claimState('weather_stockholm');
128
+ if (active) {
129
+ console.log(`${active.heldBy} is ${active.action}`);
132
130
  }
133
131
 
134
- await ablo.weatherReports.update('weather_stockholm', { status: 'ready' });
132
+ await ablo.weatherReports.claim('weather_stockholm', async (report) => {
133
+ await ablo.weatherReports.update(report.id, { status: 'ready' });
134
+ });
135
135
  ```
136
136
 
137
- `ifBusy` controls what happens when another human or agent is already working
138
- on the same target:
139
-
140
- - `return` returns immediately with active intents.
141
- - `wait` waits for the intent stream to clear.
142
- - `fail` throws `AbloBusyError` with the active intents attached.
137
+ Use `{ wait: false }` on `claim` when work should be skipped instead of queued
138
+ behind an active holder.
143
139
 
144
140
  ## 8. Next Steps
145
141
 
146
- Keep using the schema client for app and agent writes. Reach for the advanced
147
- schema-less agent wrapper only when a worker intentionally cannot import the
148
- app schema.
142
+ Keep using the schema client for app and agent writes.
149
143
 
150
144
  - [Integration Guide](./integration-guide.md) explains the full app, React, Data Source, multiplayer, and agent path.
151
145
  - [Guarantees](./guarantees.md) explains what confirmed writes and stale checks mean.
package/docs/react.md CHANGED
@@ -14,21 +14,71 @@ The React bindings ship with the main package — no extra install.
14
14
  import { useAblo } from '@abloatai/ablo/react';
15
15
  ```
16
16
 
17
- ## useAblo — model resource
17
+ ## AbloProvider
18
+
19
+ Mount it once near the root of your tree. It owns the connection, the local
20
+ pool, and the engine lifecycle; everything below it reads with `useAblo`.
21
+
22
+ ```tsx
23
+ 'use client';
24
+
25
+ import { AbloProvider } from '@abloatai/ablo/react';
26
+ import { schema } from '@/ablo/schema';
27
+
28
+ export function Providers({
29
+ children,
30
+ user, // resolved server-side from YOUR auth
31
+ }: {
32
+ children: React.ReactNode;
33
+ user: { id: string; teamIds: string[] };
34
+ }) {
35
+ return (
36
+ <AbloProvider
37
+ schema={schema}
38
+ userId={user.id}
39
+ teamIds={user.teamIds}
40
+ fallback={<AppSkeleton />}
41
+ >
42
+ {children}
43
+ </AbloProvider>
44
+ );
45
+ }
46
+ ```
47
+
48
+ `schema` is the only required prop. The rest are situational:
49
+
50
+ | Prop | Default | Purpose |
51
+ | ------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------- |
52
+ | `schema` | — | **Required.** From `defineSchema()`. Determines the typed hook surface. |
53
+ | `userId` | resolved from auth | App participant id for app-owned fields and your `identityRoles.extract`. Not the security boundary. |
54
+ | `teamIds` | resolved from auth | Team ids expanded into team sync groups via `identityRoles`. |
55
+ | `syncGroups` | full allowed set | **Narrows** the subscription to a subset of what auth allows (e.g. `['deck:abc123']`). Never widens it. |
56
+ | `url` | hosted endpoint | WebSocket URL of the sync server (`wss://…`). Hosted apps omit it. |
57
+ | `apiKey` | session/cookie | Bootstrap auth. Browser apps **omit this** — the key stays server-side. See Identity below. |
58
+ | `fallback` | neutral spinner | Rendered during the *first* bootstrap only. Pass a branded skeleton, `null`, or `'passthrough'`. |
59
+ | `bootstrapMode` | `'full'` | `'full'` pulls the org's baseline before ready; `'none'` skips the baseline and processes live deltas only.|
60
+ | `persistence` | `'volatile'` | `'indexeddb'` opts into a durable browser cache that survives reloads. |
61
+ | `onSessionExpired` | — | Fired after the engine has already purged on a rejected session — use for redirect-to-sign-in. |
62
+ | `onError` | — | Engine / WebSocket / `postBootstrap` errors. Wire to Sentry / Datadog. |
63
+
64
+ Where `userId` / `teamIds` / `syncGroups` come from, and why the API key never
65
+ reaches the browser, is the whole of
66
+ [Identity & Sync Groups](./identity.md) — read that if it isn't obvious how org
67
+ / team / user map to what a participant can see.
68
+
69
+ ## useAblo — model client
18
70
 
19
71
  ```tsx
20
72
  'use client';
21
73
 
22
74
  import { useAblo } from '@abloatai/ablo/react';
23
75
 
24
- export function TaskView({ task: serverTask }: { task: { id: string; title: string } }) {
25
- const task = useAblo((ablo) => ablo.tasks.retrieve(serverTask.id)) ?? serverTask;
26
- const intents = useAblo((ablo) =>
27
- ablo.intents.list({ resource: 'tasks', id: serverTask.id }),
28
- ) ?? [];
29
- const busy = intents.length > 0;
76
+ export function ReportView({ report: serverReport }: { report: { id: string; location: string } }) {
77
+ const report = useAblo((ablo) => ablo.weatherReports.retrieve(serverReport.id)) ?? serverReport;
78
+ const active = useAblo((ablo) => ablo.weatherReports.claimState(serverReport.id));
79
+ const claimed = Boolean(active);
30
80
 
31
- return <article>{task.title}</article>;
81
+ return <article>{report.location}</article>;
32
82
  }
33
83
  ```
34
84
 
@@ -37,9 +87,9 @@ The hook:
37
87
  1. Reads through the same `ablo.<model>` methods as the rest of the SDK.
38
88
  2. Tracks the model fields read by the selector and re-renders when confirmed
39
89
  deltas arrive.
40
- 3. Lets Server Component data stay outside the hook: use `?? serverTask` when a
90
+ 3. Lets Server Component data stay outside the hook: use `?? serverReport` when a
41
91
  parent already loaded the row.
42
- 4. Works for coordination state too, such as `ablo.intents.list(...)`.
92
+ 4. Works for coordination state too, such as `ablo.weatherReports.claimState(id)`.
43
93
 
44
94
  Use the zero-argument form only when you need the full client for callbacks,
45
95
  effects, or writes:
@@ -50,16 +100,16 @@ const abloClient = useAblo();
50
100
 
51
101
  Prefer selector reads like `useAblo((ablo) => ablo.<model>.retrieve(id))`.
52
102
  String model names are kept on older hooks for compatibility, but first examples
53
- should use the same model-resource shape as the rest of the SDK.
103
+ should use the same model-client shape as the rest of the SDK.
54
104
 
55
- For collections, keep the selector on the model resource too:
105
+ For collections, keep the selector on the model client too:
56
106
 
57
107
  ```tsx
58
- const tasks = useAblo((ablo) =>
59
- ablo.tasks.list({
108
+ const reports = useAblo((ablo) =>
109
+ ablo.weatherReports.list({
60
110
  where: { projectId },
61
- filter: (task) => task.status !== 'done',
62
- scope: 'live',
111
+ filter: (report) => report.status !== 'ready',
112
+ state: 'live',
63
113
  }),
64
114
  );
65
115
  ```
@@ -67,7 +117,7 @@ const tasks = useAblo((ablo) =>
67
117
  ## Server Load
68
118
 
69
119
  ```tsx
70
- const [task] = await ablo.tasks.load({ where: { id } });
120
+ const [report] = await ablo.weatherReports.load({ where: { id } });
71
121
  ```
72
122
 
73
123
  Use `load` in Server Components when the row may not be in the local pool yet.
@@ -79,8 +129,8 @@ For Server Actions and route handlers, call the SDK directly:
79
129
  ```ts
80
130
  import { ablo } from '@/lib/ablo';
81
131
 
82
- const snap = ablo.snapshot({ tasks: id });
83
- await ablo.tasks.update(id, patch, {
132
+ const snap = ablo.snapshot({ weatherReports: id });
133
+ await ablo.weatherReports.update(id, patch, {
84
134
  readAt: snap.stamp,
85
135
  onStale: 'reject',
86
136
  wait: 'confirmed',
@@ -88,17 +138,17 @@ await ablo.tasks.update(id, patch, {
88
138
  ```
89
139
 
90
140
  For client event handlers, get the provider-owned client and call the same
91
- model resource:
141
+ model client:
92
142
 
93
143
  ```tsx
94
144
  const ablo = useAblo();
95
145
 
96
- async function markDone() {
146
+ async function markReady() {
97
147
  if (!ablo) return;
98
- const snap = ablo.snapshot({ tasks: id });
99
- await ablo.tasks.update(
148
+ const snap = ablo.snapshot({ weatherReports: id });
149
+ await ablo.weatherReports.update(
100
150
  id,
101
- { status: 'done' },
151
+ { status: 'ready' },
102
152
  { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
103
153
  );
104
154
  }
package/docs/roadmap.md CHANGED
@@ -4,22 +4,32 @@ What is shipped, what is next, and what we will not build.
4
4
 
5
5
  ## Shipped
6
6
 
7
- - **Resources, intents, commits** — the core API.
8
- - **Capability tokens** — Biscuit-signed, scoped, attenuable, hot-revocable.
7
+ - **Models, claims, commits** — the core API.
9
8
  - **Audit log** — hash-chained per principal, with `delegationChainRoot`.
10
9
  - **MCP transport** — HTTP server at `/api/mcp`.
11
10
  - **TypeScript SDK** — `@abloatai/ablo`, with React bindings.
12
11
  - **Dashboard** — keys, audit, metrics, allowed origins.
12
+ - **Schema migrations** — declarative model schema changes. Pure
13
+ diff/classify/cast-safety/backfill planning engine (`diffSchema`,
14
+ `classifyMigration`, `classifyCast`, `isAutoApplicable`) ships in the SDK;
15
+ `ablo generate` emits typed clients from a pushed schema; the server
16
+ applies and activates migrations only on success.
17
+ - **Structured error contract** — versioned (`ERROR_CONTRACT_VERSION`),
18
+ drift-guarded code registry shared across the HTTP, WebSocket, and MCP
19
+ planes, with always-on request ids and an OpenAPI error envelope.
20
+ - **Relation-driven sync groups** — membership-routed fan-out via branded
21
+ `SyncGroup` + schema-declared identity roles (schema-JSON v2).
22
+ - **Intent coordination plane** — Stripe-shaped `Intent` with per-model
23
+ `intent(id)` handles for lease/await/commit coordination between agents.
13
24
 
14
25
  ## In flight
15
26
 
16
- - **Real-time presence** — see who else is viewing/editing a resource.
27
+ - **Real-time presence** — see who else is viewing/editing a model
28
+ (coordination primitives landed; presence surface in progress).
17
29
  - **Cross-instance fan-out via Redis** — pub/sub deltas at scale.
18
- - **Hot capability revocation UI** — in the dashboard, today via API only.
19
30
 
20
31
  ## On deck
21
32
 
22
- - **Schema migrations** — declarative resource schema changes.
23
33
  - **Field-level subscriptions** — subscribe to one path, not the whole row.
24
34
  - **Bulk import/export** — CSV/JSON round-trip with chain verification.
25
35
 
@@ -31,11 +41,11 @@ What is shipped, what is next, and what we will not build.
31
41
 
32
42
  ## We will not build
33
43
 
34
- - **A general-purpose Postgres wrapper** — Ablo Sync is for state with
44
+ - **A general-purpose Postgres wrapper** — Ablo is for state with
35
45
  concurrency semantics, not for storing every table.
36
46
  - **Server-side compute** — no triggers, no stored procedures. Compute
37
47
  belongs in your application code.
38
- - **A document database UI** — your data lives in Ablo Sync; the UI is your
48
+ - **A document database UI** — your data lives in Ablo; the UI is your
39
49
  product, not ours.
40
50
 
41
51
  ## How priorities shift