@abloatai/ablo 0.5.1 → 0.6.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 (94) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +217 -122
  3. package/dist/BaseSyncedStore.d.ts +2 -2
  4. package/dist/BaseSyncedStore.js +2 -2
  5. package/dist/api/index.d.ts +3 -3
  6. package/dist/api/index.js +1 -1
  7. package/dist/client/Ablo.d.ts +90 -93
  8. package/dist/client/Ablo.js +121 -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 +90 -87
  14. package/dist/client/createModelProxy.js +124 -127
  15. package/dist/client/index.d.ts +6 -7
  16. package/dist/client/index.js +4 -5
  17. package/dist/client/validateAbloOptions.js +3 -3
  18. package/dist/core/index.d.ts +2 -0
  19. package/dist/core/index.js +7 -0
  20. package/dist/errors.d.ts +8 -8
  21. package/dist/errors.js +18 -10
  22. package/dist/index.d.ts +9 -8
  23. package/dist/index.js +7 -11
  24. package/dist/interfaces/index.d.ts +2 -10
  25. package/dist/mutators/Transaction.d.ts +2 -2
  26. package/dist/mutators/Transaction.js +2 -2
  27. package/dist/mutators/mutateActions.d.ts +44 -0
  28. package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
  29. package/dist/mutators/readerActions.d.ts +32 -0
  30. package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
  31. package/dist/query/types.d.ts +1 -1
  32. package/dist/react/AbloProvider.d.ts +1 -1
  33. package/dist/react/AbloProvider.js +3 -3
  34. package/dist/react/context.d.ts +4 -4
  35. package/dist/react/index.d.ts +4 -5
  36. package/dist/react/index.js +3 -7
  37. package/dist/react/useAblo.d.ts +14 -14
  38. package/dist/react/useAblo.js +26 -26
  39. package/dist/react/useIntent.d.ts +2 -2
  40. package/dist/react/useIntent.js +2 -2
  41. package/dist/react/useMutators.d.ts +1 -1
  42. package/dist/react/usePresence.d.ts +3 -3
  43. package/dist/react/usePresence.js +4 -4
  44. package/dist/react/useUndoScope.d.ts +1 -1
  45. package/dist/schema/diff.d.ts +161 -0
  46. package/dist/schema/diff.js +262 -0
  47. package/dist/schema/generate.d.ts +19 -0
  48. package/dist/schema/generate.js +87 -0
  49. package/dist/schema/index.d.ts +4 -1
  50. package/dist/schema/index.js +7 -1
  51. package/dist/schema/schema.d.ts +83 -32
  52. package/dist/schema/schema.js +58 -12
  53. package/dist/schema/serialize.d.ts +92 -0
  54. package/dist/schema/serialize.js +227 -0
  55. package/dist/sync/SyncWebSocket.d.ts +17 -0
  56. package/dist/sync/SyncWebSocket.js +46 -1
  57. package/dist/sync/awaitIntentGrant.d.ts +26 -0
  58. package/dist/sync/awaitIntentGrant.js +60 -0
  59. package/dist/sync/createIntentStream.js +43 -4
  60. package/dist/sync/createPresenceStream.js +1 -1
  61. package/dist/sync/participants.d.ts +2 -2
  62. package/dist/sync/participants.js +4 -4
  63. package/dist/types/global.d.ts +43 -52
  64. package/dist/types/global.js +16 -18
  65. package/dist/types/streams.d.ts +37 -9
  66. package/docs/api.md +68 -158
  67. package/docs/audit.md +5 -5
  68. package/docs/client-behavior.md +41 -42
  69. package/docs/coordination.md +294 -0
  70. package/docs/data-sources.md +14 -14
  71. package/docs/examples/agent-human.md +30 -32
  72. package/docs/examples/ai-sdk-tool.md +32 -33
  73. package/docs/examples/existing-python-backend.md +35 -33
  74. package/docs/examples/nextjs.md +24 -25
  75. package/docs/examples/server-agent.md +20 -61
  76. package/docs/guarantees.md +30 -55
  77. package/docs/identity.md +458 -0
  78. package/docs/index.md +12 -24
  79. package/docs/integration-guide.md +106 -116
  80. package/docs/interaction-model.md +29 -95
  81. package/docs/mcp/claude-code.md +3 -3
  82. package/docs/mcp/cursor.md +1 -1
  83. package/docs/mcp/windsurf.md +1 -1
  84. package/docs/mcp.md +11 -26
  85. package/docs/quickstart.md +43 -49
  86. package/docs/react.md +73 -23
  87. package/docs/roadmap.md +5 -7
  88. package/llms.txt +34 -39
  89. package/package.json +1 -1
  90. package/dist/react/useMutate.d.ts +0 -83
  91. package/dist/react/useQuery.d.ts +0 -123
  92. package/dist/react/useQuery.js +0 -145
  93. package/dist/react/useReader.d.ts +0 -69
  94. package/docs/capabilities.md +0 -163
@@ -0,0 +1,458 @@
1
+ # Identity & Sync Groups
2
+
3
+ This is the doc the Quickstart skips: **who is connecting, and which slice
4
+ of shared state do they get?** If you've wired `<AbloProvider schema={schema}>`
5
+ and wondered where org / team / user actually come from — start here.
6
+
7
+ ## Ablo does not do auth
8
+
9
+ Ablo is not an identity provider. It has no login, no password store, no
10
+ session of its own. You keep whatever you already use — Clerk, Auth0,
11
+ NextAuth, WorkOS, your own session table. Ablo's job begins **after** you've
12
+ authenticated the user: you hand Ablo the already-authenticated identity, and
13
+ Ablo decides which **sync groups** that identity may read and write.
14
+
15
+ So the integration question is never "how do I log into Ablo?" It's: *"My app
16
+ already knows this request is user `U` in org `O`. How do I tell Ablo, so it
17
+ scopes their realtime data correctly?"* The rest of this doc answers exactly
18
+ that.
19
+
20
+ ## What a sync group is
21
+
22
+ A **sync group** is a named channel of shared state — a string like
23
+ `org:acme` or `deck:abc123`. It is simultaneously:
24
+
25
+ - **the unit of fan-out** — a confirmed write to a row publishes a delta to
26
+ every participant subscribed to that row's sync group(s), and
27
+ - **the unit of access** — a participant receives a row's deltas *only if* the
28
+ row's sync group is in their allowed set.
29
+
30
+ There is no built-in `org` / `team` / `user` concept in the engine. Those are
31
+ *your* domain words. Ablo only knows sync-group strings. The mapping from "this
32
+ is user U in org O" to "they may subscribe to `org:acme` and `user:U`" is
33
+ something **you declare in your schema** — covered next.
34
+
35
+ ### Two kinds of group — the whole mental model
36
+
37
+ Every sync group is named after one of two things, and that's the cleanest way
38
+ to hold the model in your head:
39
+
40
+ - **Membership groups** — named after *who you are*: `org:{id}`, `team:{id}`,
41
+ `user:{id}`. Produced from **identity** (`identityRoles`, Half 1). They're
42
+ standing and durable — they don't change as you work.
43
+ - **Entity groups** — named after *a thing*: `dataroom:{id}`, `deck:{id}`,
44
+ `slide:{id}`. Produced from a **row's id** (a model's entity scope, Half 2).
45
+ They're granular — one per record — and any participant can be pointed at a
46
+ specific set of them.
47
+
48
+ Humans and agents fill that same space differently — and, crucially, the two
49
+ are **declared in different places**, because membership is static (a property of
50
+ identity) while entity scope is dynamic (a property of the task):
51
+
52
+ | | Subscribed by | Declared where | Gets |
53
+ | --- | --- | --- | --- |
54
+ | **Human** | *who they are* — membership | **the schema** (`identityRoles`) — a rule, written once | every `org` / `team` / `user` group their identity implies — their whole standing world |
55
+ | **Agent** | *what it's been given* — entities | **code, at the spawn site** — chosen per run | a handful of entity groups: the dataroom it's in, the slides it has read — never beyond what its user's membership could reach |
56
+
57
+ > **One line:** humans subscribe by who they are; agents subscribe by what
58
+ > they've been given.
59
+
60
+ The asymmetry is the point. A user's org/team/user don't change per request, so
61
+ their scope is a **rule the schema derives automatically** — you never write
62
+ per-user scope code. An agent's reach depends on *what it's working on*, which is
63
+ only knowable at dispatch — so you pass its `syncGroups` **at the call site, in
64
+ code**. The schema's only job for entities is to declare *that* a model is
65
+ entity-scopable and *what its group is named* (`entity: 'deck'` → `deck:{id}`);
66
+ it never declares *which* entities a given agent gets. (A human can opt into the
67
+ same runtime narrowing — a page scoped to one deck — but by default a human's
68
+ scope is fully schema-derived.)
69
+
70
+ So an agent doesn't need a `user:{id}` standing grant. It's a participant pointed
71
+ at a few entity groups, bounded above by its triggering user's membership. That
72
+ boundary is the whole safety story, and it's covered in
73
+ [Agents are participants too](#agents-are-participants-too).
74
+
75
+ ```txt
76
+ your auth → identity { kind, userId|agentId, organizationId, teamIds }
77
+ → identityRoles (schema) → allowed sync groups
78
+ → participant receives deltas for rows in those groups
79
+ ```
80
+
81
+ The identity is a **participant** — and a participant is either a human
82
+ (`kind: 'user'`) or an agent (`kind: 'agent'`). Same shape, same path; see
83
+ [Agents are participants too](#agents-are-participants-too) below. Everything in
84
+ the next two sections applies to both.
85
+
86
+ ## Declare it, end to end
87
+
88
+ The entire declaration surface is three things: `identityRoles` (who may see
89
+ what), `syncGroupFormat` (which group a row fans out on), and an optional
90
+ `syncGroups` prop (narrowing). Here they are in one runnable place — the sections
91
+ after this explain each.
92
+
93
+ ```ts
94
+ // 1. src/ablo.schema.ts — map identity → groups, and anchor each model to a group
95
+ import { defineSchema, identityRole, model, z } from '@abloatai/ablo/schema';
96
+
97
+ export const schema = defineSchema(
98
+ {
99
+ conversations: model(
100
+ { title: z.string(), createdBy: z.string() },
101
+ {},
102
+ { orgScoped: true, syncGroupFormat: 'conversation:{id}' },
103
+ ),
104
+ decks: model(
105
+ { title: z.string(), status: z.enum(['draft', 'published']) },
106
+ {},
107
+ { orgScoped: true, syncGroupFormat: 'deck:{id}' },
108
+ ),
109
+ },
110
+ {
111
+ // Each role is pure data: a `template` and the identity `source` field to
112
+ // read. No closures — so the schema stays JSON-serializable end to end.
113
+ identityRoles: [
114
+ identityRole({ kind: 'tenant', template: 'org:{id}', source: 'organizationId' }),
115
+ identityRole({ kind: 'participant', template: 'user:{id}', source: 'userId' }),
116
+ identityRole({ kind: 'membership', template: 'team:{id}', source: 'teamIds', multi: true }),
117
+ ],
118
+ },
119
+ );
120
+ ```
121
+
122
+ ```tsx
123
+ // 2. app/providers.tsx — a HUMAN gets their full org / team scope
124
+ <AbloProvider schema={schema} userId={user.id} teamIds={user.teamIds}>
125
+ {children}
126
+ </AbloProvider>
127
+ ```
128
+
129
+ ```tsx
130
+ // 3. an AGENT run inherits its user, narrowed to the entities in play
131
+ <AbloProvider
132
+ schema={schema}
133
+ userId={user.id} // ceiling: the triggering user
134
+ syncGroups={[`conversation:${conversationId}`, `deck:${deckId}`]} // floor: just its work
135
+ >
136
+ {children}
137
+ </AbloProvider>
138
+ ```
139
+
140
+ That's the whole surface. The rest of this doc is the *why* behind each line.
141
+
142
+ ## The two halves of scoping
143
+
144
+ Scoping is two declarations that meet in the middle. One describes the
145
+ **participant** (what may I subscribe to?), the other describes each **row**
146
+ (which group does this row belong to?). A participant sees a row **iff** the
147
+ row's sync group is in the participant's allowed set.
148
+
149
+ ### Half 1 — `identityRoles`: identity → allowed groups
150
+
151
+ Declared once, on the schema, via the `identityRole({ kind, template, source })`
152
+ factory. Each role is **pure data**: a `template` with a single `{id}`
153
+ placeholder, and the `source` — the identity field to read. The engine reads
154
+ `source` off the identity *you* supply and substitutes each value into the
155
+ `template` to build the participant's allowed set. There is no hardcoded `org:` /
156
+ `user:` anywhere in the engine — the templates and sources are entirely yours.
157
+
158
+ ```ts
159
+ // src/ablo.schema.ts
160
+ import { defineSchema, identityRole, model, z } from '@abloatai/ablo/schema';
161
+
162
+ export const schema = defineSchema(
163
+ {
164
+ decks: model({
165
+ title: z.string(),
166
+ status: z.enum(['draft', 'published']),
167
+ }),
168
+ },
169
+ {
170
+ identityRoles: [
171
+ identityRole({ kind: 'tenant', template: 'org:{id}', source: 'organizationId' }),
172
+ identityRole({ kind: 'participant', template: 'user:{id}', source: 'userId' }),
173
+ // `multi: true` reads an array field — one team group per id.
174
+ identityRole({ kind: 'membership', template: 'team:{id}', source: 'teamIds', multi: true }),
175
+ ],
176
+ },
177
+ );
178
+ ```
179
+
180
+ The identity these `source` fields read is what your app resolves from its own
181
+ auth — Ablo never invents it. Roles are pure data (no closures) on purpose: a
182
+ `Schema` stays JSON-serializable end to end, so the same declaration works
183
+ in-process and on a hosted server that only ever sees the compiled JSON.
184
+
185
+ > **Single field per role.** `source` reads one field. An agent doesn't need its
186
+ > own role: it runs on behalf of a user and carries that user's `userId`, so the
187
+ > `user:{id}` role above already covers it — see
188
+ > [Agents are participants too](#agents-are-participants-too).
189
+
190
+ ### Half 2 — per-model scope: row → group
191
+
192
+ On each model's options, you declare how its rows are tenanted and which
193
+ sync-group label they fan out on.
194
+
195
+ ```ts
196
+ model(
197
+ { /* fields */ },
198
+ { /* relations */ },
199
+ {
200
+ // Rows carry organization_id; bootstrap + fan-out filter on it.
201
+ orgScoped: true,
202
+
203
+ // Per-entity anchor. Lets a session narrow into ONE row's scope,
204
+ // e.g. open a single deck: syncGroupFormat.replace('{id}', deckId).
205
+ syncGroupFormat: 'deck:{id}',
206
+ },
207
+ );
208
+ ```
209
+
210
+ For rows that don't carry `organization_id` directly but inherit tenancy
211
+ through a foreign key, use `scopedVia` rather than `orgScoped: false` — the
212
+ latter exposes the whole table cross-tenant. See
213
+ `packages/sync-engine/src/schema/model.ts` for the full option set.
214
+
215
+ ## How identity reaches Ablo — the proxy model
216
+
217
+ This is the part the README's "authenticates with the signed-in user's
218
+ session" glossed over. Concretely:
219
+
220
+ 1. **Your `ABLO_API_KEY` lives only on your trusted server**, scoped to your
221
+ account. It signs your app's relationship with Ablo. It must never reach a
222
+ browser bundle — treat it like a Stripe secret key.
223
+ 2. **Your server authenticates the user with your own system.** That's the
224
+ request that knows "this is user `U`, org `O`, teams `[...]`".
225
+ 3. **Your server hands that authenticated identity to Ablo**, and the browser
226
+ talks to the realtime plane as an already-scoped participant. The browser
227
+ never holds the API key and cannot widen its own scope — the security
228
+ boundary is the identity your **server** vouched for, not anything the client
229
+ asserts.
230
+ 4. **Ablo runs your `identityRoles` over that identity** to compute the allowed
231
+ sync groups, and the participant subscribes to exactly that set.
232
+
233
+ The Ablo web app (`apps/web`) is the reference implementation of this shape:
234
+ its server resolves the signed-in user and active organization from its own
235
+ auth, and the sync layer composes the participant's sync groups from that
236
+ resolved identity — the API key stays server-side throughout. The generic,
237
+ library-agnostic name for "my server tells Ablo which of my users is acting" is
238
+ the `Ablo-Acting-User` request dimension; the web app realizes it through its
239
+ own session, but the contract is the same: **identity is asserted by your
240
+ server, never by the browser.**
241
+
242
+ > **Why the proxy, not a client API key?** A browser is a hostile runtime. If
243
+ > the client could name its own org or sync groups, any user could read another
244
+ > tenant's data by editing a request. By keeping the API key server-side and
245
+ > deriving scope from the identity your server already authenticated, the trust
246
+ > boundary lands in the one place you control. This is the same reason
247
+ > Liveblocks resolves scope in `prepareSession` and Stripe mints ephemeral keys
248
+ > server-side.
249
+
250
+ ## Wiring the provider
251
+
252
+ The provider props carry the identity your server resolved. In a Next.js app,
253
+ resolve the user in a Server Component and pass it down:
254
+
255
+ ```tsx
256
+ // app/providers.tsx — 'use client'
257
+ import { AbloProvider } from '@abloatai/ablo/react';
258
+ import { schema } from '@/ablo.schema';
259
+
260
+ export function Providers({
261
+ children,
262
+ user, // { id, teamIds } — resolved server-side from YOUR auth
263
+ }: {
264
+ children: React.ReactNode;
265
+ user: { id: string; teamIds: string[] };
266
+ }) {
267
+ return (
268
+ <AbloProvider
269
+ schema={schema}
270
+ userId={user.id}
271
+ teamIds={user.teamIds}
272
+ fallback={<AppSkeleton />}
273
+ >
274
+ {children}
275
+ </AbloProvider>
276
+ );
277
+ }
278
+ ```
279
+
280
+ What each identity-related prop does — and just as importantly, does *not* do:
281
+
282
+ | Prop | Purpose |
283
+ | ------------ | ------------------------------------------------------------------------------------------------ |
284
+ | `userId` | App-level participant id, used for app-owned fields and read by your `identityRole` `source`. **Not** the security boundary — the server enforces scope from the authenticated request. |
285
+ | `teamIds` | Team ids expanded into team sync groups via your `identityRoles`. |
286
+ | `syncGroups` | Optional. **Narrows** the subscription to a subset of what auth already allows — it can never widen it. Use it to scope a page to one entity (e.g. `['deck:abc123']`). |
287
+
288
+ Because the server is the boundary, a client that changes `userId` to another
289
+ user's id does not gain their data — the server resolves and enforces the real
290
+ identity on the connection. The props are how your app *tells* Ablo who it
291
+ already authenticated, not how it *proves* it.
292
+
293
+ ## Agents are participants too
294
+
295
+ An agent and a human **authenticate through the exact same path** — same proxy,
296
+ same `identityRoles`, same server-enforced boundary. An agent is a participant;
297
+ the only data difference is that it carries `kind: 'agent'` and an `agentId`
298
+ where a human carries `userId`. There is no separate identity model to learn.
299
+
300
+ What differs is **authority, not identity** — and the distinction is the whole
301
+ point. An agent always runs *on behalf of* the user who set it off, so its
302
+ **ceiling is exactly that user's access**: the same conversations, messages, and
303
+ models the triggering user can reach, and nothing that user couldn't. But within
304
+ that ceiling it is **narrowed to the model instances it is touching, or has
305
+ touched** — never the user's whole org.
306
+
307
+ Scope is therefore an intersection:
308
+
309
+ ```txt
310
+ agent authority = (triggering user's allowed set) ← ceiling, inherited (on-behalf-of)
311
+ ∩ (the model instances it touches) ← floor, least privilege per run
312
+ ```
313
+
314
+ Mechanically, this is the per-model anchor from
315
+ [Half 2](#half-2--per-model-scope-row--group) doing the work. Declare an entity
316
+ anchor on the models an agent operates on:
317
+
318
+ ```ts
319
+ // a conversation and the deck an agent edits each get a per-entity group
320
+ conversations: model({ /* … */ }, {}, { syncGroupFormat: 'conversation:{id}' }),
321
+ decks: model({ /* … */ }, {}, { syncGroupFormat: 'deck:{id}' }),
322
+ ```
323
+
324
+ Then a run subscribes only to the entity groups for the rows it works on — a
325
+ subset of what its user could see:
326
+
327
+ ```tsx
328
+ // agent run triggered by `user`, working on one conversation + one deck
329
+ <AbloProvider
330
+ schema={schema}
331
+ // identity inherited from the triggering user (the ceiling)
332
+ userId={user.id}
333
+ // authority narrowed to just the entities in play (the floor)
334
+ syncGroups={[`conversation:${conversationId}`, `deck:${deckId}`]}
335
+ >
336
+ ```
337
+
338
+ As the run touches more entities its set **accretes** to cover them; it never
339
+ widens past the user's ceiling, and it carries no standing access to entities it
340
+ isn't working on. The `identityRoles` need no agent-specific entry: the agent
341
+ carries the triggering user's `userId`, so the same `user:{id}` role that scopes
342
+ a human already scopes the agent. Nothing about the *identity* declaration
343
+ branches on agent vs human.
344
+
345
+ `kind` is what attribution uses — not access. `kind: 'agent'` plus `agentId` is
346
+ connection metadata that tags every write with the executing agent **and** the
347
+ user it ran on behalf of, so audit answers "who did this, and on whose behalf."
348
+ It never appears in an `identityRole`, because it changes *who's accountable*,
349
+ not *what's reachable*.
350
+
351
+ This is deliberately the shape the 2025–2026 agent-identity consensus converged
352
+ on, expressed in Ablo's primitives rather than a bolted-on agent ACL:
353
+
354
+ - **Inherit the user, and no more** — the OAuth
355
+ [on-behalf-of](https://workos.com/blog/oauth-on-behalf-of-ai-agents) model: the
356
+ agent's reach is tied to the consenting user, never the org.
357
+ - **Least privilege, just-in-time** — scoped to the task's entities, not standing
358
+ org-wide access (the over-privilege pattern
359
+ [OWASP's NHI Top 10](https://www.token.security/assets/the-ultimate-non-human-identity-security-guide)
360
+ flags as the dominant agent risk).
361
+ - **Dual-principal attribution** — record both the executing agent and the
362
+ triggering human.
363
+
364
+ Identity is 1:1 with a human participant; authority is narrowed to the work. That
365
+ split is what lets Ablo keep *one model API for every actor* without ever
366
+ granting an agent standing access to everything its user can see. The agent that
367
+ runs the [Coordinating long agent work](../README.md#coordinating-long-agent-work)
368
+ `claim` loop is, to the scoping layer, that same participant — scoped to the row
369
+ it claimed.
370
+
371
+ ## Narrowing to a single entity
372
+
373
+ For a page that should only sync one record, combine a per-entity
374
+ `syncGroupFormat` (Half 2) with the `syncGroups` prop:
375
+
376
+ ```ts
377
+ // schema: decks fan out on deck:{id}
378
+ syncGroupFormat: 'deck:{id}'
379
+ ```
380
+
381
+ ```tsx
382
+ // page provider: subscribe to just this deck (still inside what auth allows)
383
+ <AbloProvider schema={schema} userId={user.id} syncGroups={[`deck:${deckId}`]}>
384
+ ```
385
+
386
+ The participant now receives deltas for that one deck instead of the whole org
387
+ — smaller bootstrap, less fan-out — without weakening the server-enforced
388
+ boundary.
389
+
390
+ ## How this compares — and the best practices it follows
391
+
392
+ Ablo's identity model is not novel; it's the convergent answer every serious
393
+ realtime / sync SDK arrived at. Knowing which industry pattern it *is* tells you
394
+ how to reason about it.
395
+
396
+ **Realtime authorization splits into two shapes.** Ablo is firmly in the first:
397
+
398
+ - **Server derives scope from authenticated identity** — the server decides what
399
+ a participant may read/write and the client cannot override it. This is Ablo's
400
+ proxy model. It's the same shape as
401
+ [Supabase Realtime's RLS-on-connect](https://supabase.com/docs/guides/realtime/authorization)
402
+ (policies evaluated at subscribe, cached for the connection),
403
+ [Liveblocks **ID tokens**](https://liveblocks.io/docs/authentication) ("Liveblocks
404
+ checks the permissions for you" — recommended for production), and
405
+ [ElectricSQL **proxy auth**](https://electric-sql.com/docs/guides/auth) (a
406
+ reverse-proxy sets shape params server-side before forwarding).
407
+ - **Client proposes, server authorizes the exact request** — the client names
408
+ the room/shape and the server signs off, as in
409
+ [Pusher's channel authorization endpoint](https://pusher.com/docs/channels/server_api/authorizing-users/),
410
+ [ElectricSQL **gatekeeper auth**](https://github.com/electric-sql/electric/blob/main/examples/gatekeeper-auth/README.md),
411
+ and Liveblocks **access tokens**. Ablo's `syncGroups` prop is the *narrowing*
412
+ half of this — but it can only ever shrink the server-derived set, never grow
413
+ it.
414
+
415
+ The best practices Ablo inherits from that lineage:
416
+
417
+ 1. **The secret never reaches the client.** Your `ABLO_API_KEY` lives only on a
418
+ trusted server — exactly as
419
+ [Ably mandates](https://ably.com/docs/auth/token) ("never use API keys in
420
+ client-side code; they don't expire, so once compromised they grant indefinite
421
+ access") and
422
+ [PowerSync's flow](https://docs.powersync.com/installation/authentication-setup/custom)
423
+ (app auth → backend mints a signed token → client connects with the token).
424
+
425
+ 2. **Trusted vs untrusted claims is the whole security argument.** PowerSync draws
426
+ the line precisely: [token parameters are trusted and usable for access
427
+ control; client parameters are not](https://docs.powersync.com/usage/sync-rules/advanced-topics/client-parameters).
428
+ In Ablo terms, the identity your server vouches for is the *trusted* claim that
429
+ sets scope; the provider's `userId` / `syncGroups` props are *untrusted client
430
+ input* — convenient for app-owned fields and narrowing, but never the boundary.
431
+ This is why changing `userId` in the browser grants nothing.
432
+
433
+ 3. **Scope by a hierarchical naming convention, declared once.** Ablo's
434
+ `identityRoles` templates (`org:{id}`, `team:{id}`, `deck:{id}`) are the same
435
+ idea as [Liveblocks' recommended room-id naming pattern](https://liveblocks.io/docs/authentication/access-token)
436
+ (`org:*`, `org:group:*`) and [Ably's channel capabilities](https://ably.com/docs/auth/capabilities).
437
+ Declaring the convention in one place — never composing scope strings in
438
+ consumer code — is the practice all three enforce.
439
+
440
+ 4. **Attribution and presence ride the authenticated identity.** Just as
441
+ [Pusher attaches `channel_data` to presence at auth time](https://pusher.com/docs/channels/server_api/authorizing-users/),
442
+ Ablo's participant identity (the one your server vouched for) is what powers
443
+ presence and per-write attribution — not a value the client asserts after the
444
+ fact.
445
+
446
+ The one practice that differs by deployment: short-lived, auto-refreshed bearer
447
+ tokens ([Ably](https://ably.com/docs/auth/token),
448
+ [Supabase's `access_token` refresh](https://supabase.com/docs/guides/realtime/authorization))
449
+ are the right shape when an untrusted client holds a credential directly. Ablo's
450
+ proxy model keeps the credential server-side instead, so token rotation is the
451
+ server's concern, not the browser's — the same trade ElectricSQL's proxy pattern
452
+ makes versus its gatekeeper tokens.
453
+
454
+ ## See also
455
+
456
+ - [Integration Guide](./integration-guide.md) — `identityRoles`, backing modes, and the full app path.
457
+ - [React](./react.md) — the complete `<AbloProvider>` prop surface.
458
+ - [API Keys](./api-keys.md) — server-side keys for the public API.
package/docs/index.md CHANGED
@@ -4,11 +4,11 @@ Ablo is a state control API for **humans and AI agents editing the same
4
4
  typed state in real time, with attribution, conflict handling, and
5
5
  fast cutoff**.
6
6
 
7
- It gives agents a narrow way to write production state: declare models, load current state, coordinate active work, and write with stale-state checks. Capabilities, tasks, commits, and receipts are the protocol underneath, not first-integration ceremony.
7
+ It gives agents a narrow way to write production state: declare models, load current state, coordinate active work, and write with stale-state checks.
8
8
 
9
9
  Multiplayer is not a separate product mode. If humans, server actions, and agents
10
10
  use the same `Ablo({ schema, apiKey })` client and write through
11
- `ablo.<model>`, Ablo fans out confirmed deltas, exposes active intents, and
11
+ `ablo.<model>`, Ablo fans out confirmed deltas, exposes active claims, and
12
12
  rejects stale writes for every participant.
13
13
 
14
14
  ## What you get, in three commitments
@@ -27,21 +27,15 @@ query-shaped sync).
27
27
  declared once on the schema's `identityRoles`. Consumer code never
28
28
  composes group strings. Same boundary Liveblocks (`prepareSession`),
29
29
  PowerSync (named streams), and Zero (synced queries) settled on.
30
- - **Capabilities, not API keys, for agents.** Per-run, per-scope, leased
31
- credentials with per-request signature verification and instant
32
- revocation. The 2025-26 AI-agent auth consensus (OAuth 2.1 / MCP,
33
- AWS STS, Vault leases, Auth0 Token Vault) converged on this shape;
34
- capabilities are Ablo's instance. See
35
- [Capabilities](./capabilities.md#why-capabilities-not-api-keys) for
36
- the design rationale.
37
30
 
38
31
  ## Start here
39
32
 
40
33
  - [Quickstart](./quickstart.md) — Make your first schema-backed write.
34
+ - [Identity & Sync Groups](./identity.md) — Bring your own auth; tell Ablo who's connecting and how org / team / user map to sync-group scope.
41
35
  - [Integration Guide](./integration-guide.md) — Choose Ablo-managed state, Data Source, React, multiplayer, and agent patterns.
42
- - [Guarantees](./guarantees.md) — What confirmed writes, stale checks, and intents guarantee.
43
- - [Interaction Model](./interaction-model.md) — The commit plane and control plane.
44
- - [API Reference](./api.md) — Resource-by-resource method shape.
36
+ - [Guarantees](./guarantees.md) — What confirmed writes, stale checks, and claims guarantee.
37
+ - [Interaction Model](./interaction-model.md) — The schema, claim, update, confirmation loop.
38
+ - [API Reference](./api.md) — Model-by-model method shape.
45
39
  - [Client Behavior](./client-behavior.md) — Options, errors, retries, timeouts, and imports.
46
40
  - [Connect Your Database](./data-sources.md) — Keep canonical rows in your app database without giving Ablo database credentials.
47
41
  - [API Keys](./api-keys.md) — Bearer tokens for the public API.
@@ -50,15 +44,13 @@ query-shaped sync).
50
44
 
51
45
  | Plane | Primitives | Purpose |
52
46
  |---|---|---|
53
- | State | `Schema`, `Model`, `Intent`, `Receipt` | The product path. Load, coordinate, write, confirm. |
54
- | Commit | `Resource`, `Commit` | Advanced protocol path for custom runtimes and batches. |
55
- | Control | `Capability`, `Task`, `Usage` | The authority path. Scope, attribute, meter, audit. |
47
+ | State | `Schema`, `Model`, `Claim`, `Receipt` | The product path. Load, coordinate, write, confirm. |
56
48
  | Storage | `Managed State`, `Data Source` | Ablo stores declared models by default; existing app tables use a signed Data Source. |
57
49
 
58
50
  ## Use cases
59
51
 
60
52
  - **Let agents write to shared state** — Give an AI agent scoped, revocable write access to your typed data.
61
- - **Coordinate multiple actors** — Use intents to broadcast pre-write declarations across humans and agents.
53
+ - **Coordinate multiple actors** — Use claims to show pre-write work across humans and agents.
62
54
  - **Audit every agent action** — Trace any write back to a human in one query.
63
55
  - **Build collaborative editors** — Humans and agents on the same record, with realtime updates and stale-read protection.
64
56
  - **Meter and gate API usage** — Per-key, per-team usage reports and quota enforcement.
@@ -69,26 +61,22 @@ query-shaped sync).
69
61
  - [Model Methods](./api.md#model-methods) — Load and write typed state.
70
62
  - [Integration Guide](./integration-guide.md) — The normal app path and optional pieces.
71
63
  - [Guarantees](./guarantees.md) — Confirmed writes, optimistic state, stale-write protection, and agent lifecycle.
72
- - [Intent](./api.md#intent) — Broadcast proposed work before committing.
73
- - [Advanced Commit API](./api.md#advanced-commit-api) — Apply custom batch mutations.
64
+ - [Coordination](./coordination.md) — `claim`, `claimState`, and `queue` for active work.
74
65
  - [Connect Your Database](./data-sources.md) — Where data lands when your app database is canonical.
75
66
  - [Receipt](./api.md#receipt) — Confirm what landed.
76
- - [Capability](./api.md#capability) — Signed credentials for a bounded actor, operation set, sync group, and lease.
77
- - [Task](./api.md#task) — One agent run; the audit envelope for commits and cost.
78
67
  - [Usage](./api.md#usage) — Metering and audit dimensions.
79
68
 
80
69
  ## Examples
81
70
 
82
71
  - [AI SDK Tool](./examples/ai-sdk-tool.md) — Put Ablo inside an AI SDK tool call.
83
72
  - [Existing Python Backend](./examples/existing-python-backend.md) — Add multiplayer and future agent writes without replacing a Python API server.
84
- - [Agent + Human](./examples/agent-human.md) — Yield when a human is editing the same task.
85
- - [Server Agent](./examples/server-agent.md) — Schema-backed worker plus advanced schema-less run.
73
+ - [Agent + Human](./examples/agent-human.md) — Yield when a human is editing the same report.
74
+ - [Server Agent](./examples/server-agent.md) — Schema-backed worker.
86
75
  - [Next.js](./examples/nextjs.md) — App-router setup with React bindings.
87
76
 
88
77
  ## Runtime builds
89
78
 
90
- - `@abloatai/ablo` — schema-powered sync client for typed model operations, realtime, intents, and receipts.
91
- - `Ablo({ apiKey })` — advanced resource client for runtimes that intentionally cannot import a schema.
79
+ - `@abloatai/ablo` — schema-powered sync client for typed model operations, realtime claims, and receipts.
92
80
 
93
81
  ## More
94
82