@abloatai/ablo 0.3.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 (278) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +12 -0
  4. package/README.md +230 -0
  5. package/dist/BaseSyncedStore.d.ts +709 -0
  6. package/dist/BaseSyncedStore.js +1843 -0
  7. package/dist/Database.d.ts +344 -0
  8. package/dist/Database.js +1259 -0
  9. package/dist/LazyReferenceCollection.d.ts +181 -0
  10. package/dist/LazyReferenceCollection.js +460 -0
  11. package/dist/Model.d.ts +339 -0
  12. package/dist/Model.js +715 -0
  13. package/dist/ModelRegistry.d.ts +200 -0
  14. package/dist/ModelRegistry.js +535 -0
  15. package/dist/NetworkMonitor.d.ts +27 -0
  16. package/dist/NetworkMonitor.js +73 -0
  17. package/dist/ObjectPool.d.ts +202 -0
  18. package/dist/ObjectPool.js +1106 -0
  19. package/dist/SyncClient.d.ts +489 -0
  20. package/dist/SyncClient.js +1555 -0
  21. package/dist/SyncEngineContext.d.ts +46 -0
  22. package/dist/SyncEngineContext.js +74 -0
  23. package/dist/adapters/alwaysOnline.d.ts +16 -0
  24. package/dist/adapters/alwaysOnline.js +19 -0
  25. package/dist/adapters/inMemoryStorage.d.ts +30 -0
  26. package/dist/adapters/inMemoryStorage.js +94 -0
  27. package/dist/agent/Agent.d.ts +358 -0
  28. package/dist/agent/Agent.js +500 -0
  29. package/dist/agent/index.d.ts +115 -0
  30. package/dist/agent/index.js +128 -0
  31. package/dist/agent/session.d.ts +90 -0
  32. package/dist/agent/session.js +156 -0
  33. package/dist/agent/types.d.ts +73 -0
  34. package/dist/agent/types.js +10 -0
  35. package/dist/ai-sdk/coordination-context.d.ts +51 -0
  36. package/dist/ai-sdk/coordination-context.js +107 -0
  37. package/dist/ai-sdk/index.d.ts +68 -0
  38. package/dist/ai-sdk/index.js +68 -0
  39. package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
  40. package/dist/ai-sdk/intent-broadcast.js +72 -0
  41. package/dist/ai-sdk/wrap.d.ts +67 -0
  42. package/dist/ai-sdk/wrap.js +45 -0
  43. package/dist/api/index.d.ts +10 -0
  44. package/dist/api/index.js +9 -0
  45. package/dist/auth/index.d.ts +137 -0
  46. package/dist/auth/index.js +246 -0
  47. package/dist/client/Ablo.d.ts +835 -0
  48. package/dist/client/Ablo.js +1440 -0
  49. package/dist/client/ApiClient.d.ts +200 -0
  50. package/dist/client/ApiClient.js +659 -0
  51. package/dist/client/auth.d.ts +79 -0
  52. package/dist/client/auth.js +81 -0
  53. package/dist/client/createInternalComponents.d.ts +44 -0
  54. package/dist/client/createInternalComponents.js +88 -0
  55. package/dist/client/createModelProxy.d.ts +152 -0
  56. package/dist/client/createModelProxy.js +199 -0
  57. package/dist/client/identity.d.ts +63 -0
  58. package/dist/client/identity.js +156 -0
  59. package/dist/client/index.d.ts +36 -0
  60. package/dist/client/index.js +33 -0
  61. package/dist/client/persistence.d.ts +7 -0
  62. package/dist/client/persistence.js +11 -0
  63. package/dist/client/validateAbloOptions.d.ts +42 -0
  64. package/dist/client/validateAbloOptions.js +43 -0
  65. package/dist/config/index.d.ts +10 -0
  66. package/dist/config/index.js +12 -0
  67. package/dist/context.d.ts +27 -0
  68. package/dist/context.js +58 -0
  69. package/dist/core/DatabaseManager.d.ts +108 -0
  70. package/dist/core/DatabaseManager.js +361 -0
  71. package/dist/core/QueryProcessor.d.ts +77 -0
  72. package/dist/core/QueryProcessor.js +262 -0
  73. package/dist/core/QueryView.d.ts +64 -0
  74. package/dist/core/QueryView.js +219 -0
  75. package/dist/core/StoreManager.d.ts +131 -0
  76. package/dist/core/StoreManager.js +334 -0
  77. package/dist/core/ViewRegistry.d.ts +20 -0
  78. package/dist/core/ViewRegistry.js +55 -0
  79. package/dist/core/index.d.ts +34 -0
  80. package/dist/core/index.js +59 -0
  81. package/dist/core/openIDBWithTimeout.d.ts +27 -0
  82. package/dist/core/openIDBWithTimeout.js +63 -0
  83. package/dist/core/query-utils.d.ts +37 -0
  84. package/dist/core/query-utils.js +60 -0
  85. package/dist/errors.d.ts +235 -0
  86. package/dist/errors.js +243 -0
  87. package/dist/index.d.ts +41 -0
  88. package/dist/index.js +82 -0
  89. package/dist/interfaces/headless.d.ts +95 -0
  90. package/dist/interfaces/headless.js +41 -0
  91. package/dist/interfaces/index.d.ts +321 -0
  92. package/dist/interfaces/index.js +8 -0
  93. package/dist/mutators/RecordingTransaction.d.ts +36 -0
  94. package/dist/mutators/RecordingTransaction.js +216 -0
  95. package/dist/mutators/Transaction.d.ts +48 -0
  96. package/dist/mutators/Transaction.js +64 -0
  97. package/dist/mutators/UndoManager.d.ts +114 -0
  98. package/dist/mutators/UndoManager.js +143 -0
  99. package/dist/mutators/defineMutators.d.ts +55 -0
  100. package/dist/mutators/defineMutators.js +28 -0
  101. package/dist/policy/index.d.ts +19 -0
  102. package/dist/policy/index.js +18 -0
  103. package/dist/policy/types.d.ts +74 -0
  104. package/dist/policy/types.js +17 -0
  105. package/dist/principal.d.ts +44 -0
  106. package/dist/principal.js +49 -0
  107. package/dist/query/client.d.ts +43 -0
  108. package/dist/query/client.js +84 -0
  109. package/dist/query/index.d.ts +6 -0
  110. package/dist/query/index.js +5 -0
  111. package/dist/query/types.d.ts +143 -0
  112. package/dist/query/types.js +36 -0
  113. package/dist/react/AbloProvider.d.ts +205 -0
  114. package/dist/react/AbloProvider.js +398 -0
  115. package/dist/react/ClientSideSuspense.d.ts +36 -0
  116. package/dist/react/ClientSideSuspense.js +17 -0
  117. package/dist/react/DefaultFallback.d.ts +24 -0
  118. package/dist/react/DefaultFallback.js +43 -0
  119. package/dist/react/SyncGroupProvider.d.ts +19 -0
  120. package/dist/react/SyncGroupProvider.js +44 -0
  121. package/dist/react/context.d.ts +161 -0
  122. package/dist/react/context.js +35 -0
  123. package/dist/react/index.d.ts +64 -0
  124. package/dist/react/index.js +73 -0
  125. package/dist/react/internalContext.d.ts +35 -0
  126. package/dist/react/internalContext.js +3 -0
  127. package/dist/react/useAblo.d.ts +72 -0
  128. package/dist/react/useAblo.js +63 -0
  129. package/dist/react/useCurrentUserId.d.ts +21 -0
  130. package/dist/react/useCurrentUserId.js +33 -0
  131. package/dist/react/useErrorListener.d.ts +20 -0
  132. package/dist/react/useErrorListener.js +39 -0
  133. package/dist/react/useIntent.d.ts +29 -0
  134. package/dist/react/useIntent.js +42 -0
  135. package/dist/react/useMutate.d.ts +83 -0
  136. package/dist/react/useMutate.js +122 -0
  137. package/dist/react/useMutationFailureListener.d.ts +26 -0
  138. package/dist/react/useMutationFailureListener.js +38 -0
  139. package/dist/react/useMutators.d.ts +56 -0
  140. package/dist/react/useMutators.js +66 -0
  141. package/dist/react/usePresence.d.ts +32 -0
  142. package/dist/react/usePresence.js +41 -0
  143. package/dist/react/useQuery.d.ts +123 -0
  144. package/dist/react/useQuery.js +145 -0
  145. package/dist/react/useReactive.d.ts +35 -0
  146. package/dist/react/useReactive.js +111 -0
  147. package/dist/react/useReader.d.ts +69 -0
  148. package/dist/react/useReader.js +73 -0
  149. package/dist/react/useSyncStatus.d.ts +61 -0
  150. package/dist/react/useSyncStatus.js +76 -0
  151. package/dist/react/useUndoScope.d.ts +36 -0
  152. package/dist/react/useUndoScope.js +73 -0
  153. package/dist/realtime/index.d.ts +10 -0
  154. package/dist/realtime/index.js +9 -0
  155. package/dist/schema/field.d.ts +134 -0
  156. package/dist/schema/field.js +264 -0
  157. package/dist/schema/index.d.ts +29 -0
  158. package/dist/schema/index.js +38 -0
  159. package/dist/schema/model.d.ts +326 -0
  160. package/dist/schema/model.js +89 -0
  161. package/dist/schema/queries.d.ts +203 -0
  162. package/dist/schema/queries.js +145 -0
  163. package/dist/schema/relation.d.ts +172 -0
  164. package/dist/schema/relation.js +104 -0
  165. package/dist/schema/schema.d.ts +259 -0
  166. package/dist/schema/schema.js +188 -0
  167. package/dist/schema/sugar.d.ts +129 -0
  168. package/dist/schema/sugar.js +94 -0
  169. package/dist/source/index.d.ts +423 -0
  170. package/dist/source/index.js +320 -0
  171. package/dist/source/pushQueue.d.ts +112 -0
  172. package/dist/source/pushQueue.js +249 -0
  173. package/dist/stores/ObjectStore.d.ts +103 -0
  174. package/dist/stores/ObjectStore.js +371 -0
  175. package/dist/stores/ObjectStoreContract.d.ts +39 -0
  176. package/dist/stores/ObjectStoreContract.js +1 -0
  177. package/dist/stores/SyncActionStore.d.ts +101 -0
  178. package/dist/stores/SyncActionStore.js +481 -0
  179. package/dist/sync/BootstrapHelper.d.ts +127 -0
  180. package/dist/sync/BootstrapHelper.js +434 -0
  181. package/dist/sync/ConnectionManager.d.ts +136 -0
  182. package/dist/sync/ConnectionManager.js +465 -0
  183. package/dist/sync/HydrationCoordinator.d.ts +137 -0
  184. package/dist/sync/HydrationCoordinator.js +468 -0
  185. package/dist/sync/NetworkProbe.d.ts +43 -0
  186. package/dist/sync/NetworkProbe.js +113 -0
  187. package/dist/sync/OfflineFlush.d.ts +9 -0
  188. package/dist/sync/OfflineFlush.js +22 -0
  189. package/dist/sync/OfflineTransactionStore.d.ts +37 -0
  190. package/dist/sync/OfflineTransactionStore.js +263 -0
  191. package/dist/sync/SyncWebSocket.d.ts +663 -0
  192. package/dist/sync/SyncWebSocket.js +1336 -0
  193. package/dist/sync/createIntentStream.d.ts +33 -0
  194. package/dist/sync/createIntentStream.js +243 -0
  195. package/dist/sync/createPresenceStream.d.ts +46 -0
  196. package/dist/sync/createPresenceStream.js +192 -0
  197. package/dist/sync/createSnapshot.d.ts +33 -0
  198. package/dist/sync/createSnapshot.js +124 -0
  199. package/dist/sync/participants.d.ts +114 -0
  200. package/dist/sync/participants.js +336 -0
  201. package/dist/sync/schemas.d.ts +79 -0
  202. package/dist/sync/schemas.js +78 -0
  203. package/dist/testing/fixtures/bootstrap.d.ts +45 -0
  204. package/dist/testing/fixtures/bootstrap.js +53 -0
  205. package/dist/testing/fixtures/deltas.d.ts +86 -0
  206. package/dist/testing/fixtures/deltas.js +139 -0
  207. package/dist/testing/fixtures/models.d.ts +82 -0
  208. package/dist/testing/fixtures/models.js +270 -0
  209. package/dist/testing/helpers/react-wrapper.d.ts +66 -0
  210. package/dist/testing/helpers/react-wrapper.js +64 -0
  211. package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
  212. package/dist/testing/helpers/sync-engine-harness.js +70 -0
  213. package/dist/testing/helpers/wait.d.ts +25 -0
  214. package/dist/testing/helpers/wait.js +44 -0
  215. package/dist/testing/index.d.ts +21 -0
  216. package/dist/testing/index.js +32 -0
  217. package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
  218. package/dist/testing/mocks/MockMutationExecutor.js +139 -0
  219. package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
  220. package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
  221. package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
  222. package/dist/testing/mocks/MockSyncContext.js +100 -0
  223. package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
  224. package/dist/testing/mocks/MockSyncStore.js +171 -0
  225. package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
  226. package/dist/testing/mocks/MockWebSocket.js +117 -0
  227. package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
  228. package/dist/transactions/OptimisticEchoTracker.js +104 -0
  229. package/dist/transactions/TransactionQueue.d.ts +499 -0
  230. package/dist/transactions/TransactionQueue.js +1895 -0
  231. package/dist/transactions/index.d.ts +16 -0
  232. package/dist/transactions/index.js +7 -0
  233. package/dist/transactions/mutation-error-handler.d.ts +5 -0
  234. package/dist/transactions/mutation-error-handler.js +39 -0
  235. package/dist/types/global.d.ts +107 -0
  236. package/dist/types/global.js +38 -0
  237. package/dist/types/index.d.ts +241 -0
  238. package/dist/types/index.js +70 -0
  239. package/dist/types/streams.d.ts +495 -0
  240. package/dist/types/streams.js +11 -0
  241. package/dist/utils/asyncIterator.d.ts +41 -0
  242. package/dist/utils/asyncIterator.js +142 -0
  243. package/dist/utils/duration.d.ts +28 -0
  244. package/dist/utils/duration.js +47 -0
  245. package/dist/utils/mobx-setup.d.ts +42 -0
  246. package/dist/utils/mobx-setup.js +381 -0
  247. package/docs/api-keys.md +24 -0
  248. package/docs/api.md +230 -0
  249. package/docs/audit.md +81 -0
  250. package/docs/capabilities.md +163 -0
  251. package/docs/client-behavior.md +202 -0
  252. package/docs/data-sources.md +214 -0
  253. package/docs/examples/agent-human.md +84 -0
  254. package/docs/examples/ai-sdk-tool.md +92 -0
  255. package/docs/examples/existing-python-backend.md +249 -0
  256. package/docs/examples/nextjs.md +88 -0
  257. package/docs/examples/server-agent.md +86 -0
  258. package/docs/guarantees.md +148 -0
  259. package/docs/index.md +97 -0
  260. package/docs/integration-guide.md +493 -0
  261. package/docs/interaction-model.md +140 -0
  262. package/docs/mcp/claude-code.md +43 -0
  263. package/docs/mcp/cursor.md +53 -0
  264. package/docs/mcp/windsurf.md +46 -0
  265. package/docs/mcp.md +59 -0
  266. package/docs/quickstart.md +152 -0
  267. package/docs/react.md +115 -0
  268. package/docs/roadmap.md +45 -0
  269. package/examples/README.md +54 -0
  270. package/examples/data-source/README.md +102 -0
  271. package/examples/data-source/ablo-driver.ts +89 -0
  272. package/examples/data-source/customer-server.ts +208 -0
  273. package/examples/data-source/run.ts +101 -0
  274. package/examples/data-source/schema.ts +25 -0
  275. package/examples/quickstart.ts +54 -0
  276. package/examples/tsconfig.json +16 -0
  277. package/llms.txt +143 -0
  278. package/package.json +147 -0
@@ -0,0 +1,493 @@
1
+ # Integration Guide
2
+
3
+ Use this guide when you are adding Ablo to a real product, not a demo.
4
+
5
+ ## Why Ablo, before the API
6
+
7
+ Ablo is a sync engine designed from the ground up for **humans and AI
8
+ agents editing the same state at the same time**. That premise drives
9
+ every design choice in this guide; if you only need server-to-server
10
+ data syncing without agents in the loop, the trade-offs land elsewhere
11
+ (Replicache, ElectricSQL, PowerSync are good answers for human-only
12
+ real-time apps; Zero is a good answer for query-shaped sync).
13
+
14
+ The shape of the SDK reflects three commitments:
15
+
16
+ - **One model API for every actor.** `ablo.<model>.update(...)` is what
17
+ React components, server actions, background workers, and AI agents
18
+ all call. No separate "agent SDK," no parallel mutation path. The
19
+ attribution comes from the credential, not the call site.
20
+ - **Server owns the scope convention; client picks a subset by id.** The
21
+ `org:` / `user:` / `team:` (or your own `region:` / `customer:`)
22
+ prefixes live in the schema's `identityRoles` once, never typed by
23
+ consumer code. Same boundary Liveblocks (`prepareSession`), PowerSync
24
+ (named streams), and Zero (synced queries) settled on after the same
25
+ realization: clients that compose scope strings drift; servers that
26
+ derive scope from authed identity don't.
27
+ - **Capabilities, not API keys, are how agents authenticate.** Static
28
+ API keys protect server-to-server humans. Agents get per-run,
29
+ per-scope, leased credentials with per-request signature verification
30
+ and instant revocation. The 2025-2026 AI-agent auth consensus
31
+ (OAuth 2.1 / MCP, AWS STS, Vault leases, Auth0 Token Vault) converged
32
+ on this shape. Capabilities are Ablo's instance.
33
+
34
+ If you have already built a sync layer or an agent runtime, you know
35
+ what each of those costs. This guide assumes you want them solved once,
36
+ together, behind one client.
37
+
38
+ ## The integration in one diagram
39
+
40
+ The normal integration is one client:
41
+
42
+ ```ts
43
+ import Ablo from '@ablo/sync-engine';
44
+ import { defineSchema, model, z } from '@ablo/sync-engine/schema';
45
+ ```
46
+
47
+ Declare the models Ablo coordinates, then read and write through
48
+ `ablo.<model>`. React, server actions, backend workers, and agents should all use
49
+ that same model path.
50
+
51
+ ```txt
52
+ schema -> ablo.<model>.load(...) -> ablo.<model>.update(...)
53
+ ```
54
+
55
+ Capabilities, tasks, commits, and receipts exist under the hood. Most apps do
56
+ not create them by hand.
57
+
58
+ ## Pick The Backing Mode
59
+
60
+ Every schema model has a backing store. The SDK call shape stays the same.
61
+
62
+ | Mode | Rows live in | Use when |
63
+ |---|---|---|
64
+ | Ablo-managed | Ablo | New collaborative or agent-written state can live in Ablo. |
65
+ | Data Source | Your app database | You already have tables, service logic, and API endpoints that remain canonical. |
66
+ | Schema-less resource API | Custom runtime | A server worker, MCP route, or migration script intentionally cannot import the app schema. |
67
+
68
+ Do not pass a database URL to `Ablo(...)`. Application and agent code use
69
+ `ABLO_API_KEY`. If your database stays canonical, add a Data Source URL in Ablo
70
+ and keep the database credentials inside your app.
71
+
72
+ ## Test With Sandboxes
73
+
74
+ Use the public `/sandbox` page to understand the state flow. It is a visual,
75
+ deterministic demo; it does not call your API key or mutate hosted Ablo data.
76
+ It is also built for coding agents: copy the sandbox prompt into Claude Code or
77
+ Codex and ask it to wire one real resource through the schema model API.
78
+
79
+ Use the authenticated org dashboard sandbox for real integration work. The
80
+ default sandbox is the equivalent of Stripe test mode:
81
+
82
+ - it is scoped to the organization,
83
+ - it has an isolated sync group prefix,
84
+ - it mints `sk_test_*` keys,
85
+ - it can be reset without touching live state,
86
+ - additional sandboxes can start blank or from copied live configuration.
87
+
88
+ Live keys and sandbox keys are separate. Use `sk_test_*` while wiring your app,
89
+ agents, and Data Source endpoint; move to `sk_live_*` only when the same schema
90
+ and write path are ready for production.
91
+
92
+ When handing this to a coding agent, give it a concrete target:
93
+
94
+ ```txt
95
+ Add Ablo Sync to this app for one resource that humans and agents both edit.
96
+ Use the org sandbox sk_test_* key. Declare schema, add the Ablo client, replace
97
+ one write with ablo.<model>.update(..., { readAt, onStale: 'reject',
98
+ wait: 'confirmed' }), and add a smoke test for two concurrent writers.
99
+ ```
100
+
101
+ ## 1. Declare A Schema
102
+
103
+ Start with fields and relations. Keep load strategies, indexing hints, and
104
+ read-only/mutable shortcuts out of the first version unless you already need
105
+ offline-heavy local cache behavior.
106
+
107
+ ```ts
108
+ // src/ablo.schema.ts
109
+ import { defineSchema, model, z } from '@ablo/sync-engine/schema';
110
+
111
+ export const schema = defineSchema(
112
+ {
113
+ tasks: model({
114
+ id: z.string(),
115
+ projectId: z.string(),
116
+ title: z.string(),
117
+ status: z.enum(['todo', 'doing', 'done']),
118
+ assigneeId: z.string().nullable(),
119
+ updatedAt: z.string(),
120
+ }),
121
+ },
122
+ {
123
+ // Identity-anchored sync-group roles. The server walks these to
124
+ // build each participant's allowed subscription set from the
125
+ // resolved identity context. Templates and extractors are fully
126
+ // consumer-controlled — no hardcoded `org:` / `user:` convention
127
+ // anywhere in the engine. Omit `identityRoles` entirely if your
128
+ // schema doesn't need identity-derived scoping.
129
+ identityRoles: [
130
+ {
131
+ kind: 'tenant',
132
+ template: 'org:{id}',
133
+ extract: (i) => (i.organizationId ? [String(i.organizationId)] : []),
134
+ },
135
+ {
136
+ kind: 'participant',
137
+ template: 'user:{id}',
138
+ extract: (i) => (i.userId ? [String(i.userId)] : []),
139
+ },
140
+ ],
141
+ },
142
+ );
143
+ ```
144
+
145
+ ### Declaring scope on a model
146
+
147
+ Per-row tenancy and per-entity sync-group anchors live on the
148
+ `defineModel` (or `model(...)`) options. The two halves compose: the
149
+ identity roles above produce a participant's *allowed* set; the
150
+ per-model options below define how rows are filtered server-side and
151
+ which sync-group label each row fans out on.
152
+
153
+ ```ts
154
+ model(
155
+ { /* fields */ },
156
+ /* relations */ {},
157
+ {
158
+ // Rows carry organization_id and bootstrap filters on it.
159
+ orgScoped: true,
160
+
161
+ // Per-entity sync-group anchor. Lets a capability narrow into
162
+ // one row's scope via `syncGroupFormat.replace('{id}', rowId)`.
163
+ syncGroupFormat: 'matter:{id}',
164
+ },
165
+ )
166
+ ```
167
+
168
+ For rows that don't carry `organization_id` themselves but inherit
169
+ tenancy via a foreign key, use `scopedVia` instead of `orgScoped:
170
+ false` — the latter exposes the entire table cross-tenant. See
171
+ `packages/sync-engine/src/schema/model.ts` for the full option set.
172
+
173
+ ## 2. Create The Client
174
+
175
+ Trusted runtimes can use `ABLO_API_KEY`.
176
+
177
+ ```ts
178
+ // src/ablo.ts
179
+ import Ablo from '@ablo/sync-engine';
180
+ import { schema } from './ablo.schema';
181
+
182
+ export const ablo = Ablo({
183
+ schema,
184
+ apiKey: process.env.ABLO_API_KEY,
185
+ });
186
+ ```
187
+
188
+ Browser apps should use the React provider or a scoped session/capability, not a
189
+ server API key in the bundle.
190
+
191
+ ```tsx
192
+ // app/providers.tsx
193
+ 'use client';
194
+
195
+ import { AbloProvider } from '@ablo/sync-engine/react';
196
+ import { schema } from '@/ablo.schema';
197
+
198
+ export function Providers({ children }: { children: React.ReactNode }) {
199
+ return <AbloProvider schema={schema}>{children}</AbloProvider>;
200
+ }
201
+ ```
202
+
203
+ ### Why two credential shapes
204
+
205
+ `ABLO_API_KEY` is your long-lived account credential. Treat it like a
206
+ Stripe secret key: it stays on trusted servers, never reaches a browser
207
+ bundle, and signs server-to-server requests. It is the right credential
208
+ for trusted runtimes (Next.js server actions, background workers,
209
+ migration scripts) where the code reading it is yours.
210
+
211
+ A browser or an agent runtime is not that environment. The React
212
+ provider and the `agent.run(...)` wrapper both exchange your api key for
213
+ a **capability** — a short-lived, narrowly-scoped bearer token. The
214
+ browser holds the cap; the api key never leaves the server. The
215
+ exchange is the bridge between two credential shapes:
216
+
217
+ ```
218
+ trusted runtime browser / agent
219
+ ABLO_API_KEY ─exchange─► Capability token ────► narrow scope, leased
220
+ (long-lived, (short-lived,
221
+ broad scope, per-actor scope,
222
+ server only) revocable)
223
+ ```
224
+
225
+ This is the same shape as Stripe's
226
+ ephemeral keys (Issuing Elements expires in 15 minutes) and AWS STS
227
+ AssumeRole (returns time-bounded creds with the minimal needed scope).
228
+ You never *type* a capability into your app; the SDK mints one when it
229
+ needs one and refreshes before expiry. See
230
+ [Capabilities](./capabilities.md) for the design rationale and the
231
+ manual create/revoke surface that custom runtimes use.
232
+
233
+ ## 3. Read State
234
+
235
+ Use `load` when the row may not already be local.
236
+
237
+ ```ts
238
+ await ablo.ready();
239
+
240
+ const [task] = await ablo.tasks.load({ where: { id: 'task_123' } });
241
+ if (!task) throw new Error('task not found');
242
+ ```
243
+
244
+ Use `retrieve`, `list`, and `count` for synchronous local reads after data has
245
+ loaded.
246
+
247
+ ```ts
248
+ const task = ablo.tasks.retrieve('task_123');
249
+ const activeTasks = ablo.tasks.list({
250
+ where: { projectId: 'proj_123' },
251
+ filter: (task) => task.status !== 'done',
252
+ orderBy: { updatedAt: 'desc' },
253
+ limit: 50,
254
+ });
255
+ ```
256
+
257
+ In React, selector `useAblo` is the public read API:
258
+
259
+ ```tsx
260
+ 'use client';
261
+
262
+ import { useAblo } from '@ablo/sync-engine/react';
263
+
264
+ export function TaskRow({ task: serverTask }: { task: Task }) {
265
+ const task = useAblo((ablo) => ablo.tasks.retrieve(serverTask.id)) ?? serverTask;
266
+ const intents = useAblo((ablo) =>
267
+ ablo.intents.list({ resource: 'tasks', id: serverTask.id }),
268
+ ) ?? [];
269
+
270
+ return (
271
+ <button disabled={intents.length > 0 || task.status === 'done'}>
272
+ {task.title}
273
+ </button>
274
+ );
275
+ }
276
+ ```
277
+
278
+ Use zero-argument `useAblo()` only in callbacks and effects:
279
+
280
+ ```tsx
281
+ const ablo = useAblo();
282
+ ```
283
+
284
+ ## 4. Write State
285
+
286
+ For simple writes:
287
+
288
+ ```ts
289
+ await ablo.tasks.update(
290
+ 'task_123',
291
+ { status: 'done' },
292
+ { wait: 'confirmed' },
293
+ );
294
+ ```
295
+
296
+ For writes based on state the user or agent already read, snapshot first and
297
+ reject stale updates:
298
+
299
+ ```ts
300
+ const snap = ablo.snapshot({ tasks: 'task_123' });
301
+
302
+ await ablo.tasks.update(
303
+ 'task_123',
304
+ { status: 'done' },
305
+ {
306
+ readAt: snap.stamp,
307
+ onStale: 'reject',
308
+ wait: 'confirmed',
309
+ },
310
+ );
311
+ ```
312
+
313
+ `wait: 'confirmed'` resolves after the server accepts the write. Rejections roll
314
+ back optimistic local state and throw a typed `AbloError`.
315
+
316
+ ## 5. Multiplayer Is Automatic
317
+
318
+ There is no separate multiplayer setup.
319
+
320
+ If humans, server actions, and agents use the same schema model resource, they
321
+ share the same stream:
322
+
323
+ ```txt
324
+ human UI -> ablo.tasks.update(...)
325
+ agent -> ablo.tasks.update(...)
326
+ server -> ablo.tasks.update(...)
327
+ ```
328
+
329
+ Ablo coordinates those writes, fans out confirmed deltas, exposes active intents,
330
+ and lets callers reject stale writes with `readAt`.
331
+
332
+ Direct writes to your own database bypass that stream until your app reports the
333
+ change through Data Source events.
334
+
335
+ ## 6. Existing API Backend
336
+
337
+ This is the path for a product where buttons already call Python, Rails, Go, or
338
+ Node endpoints.
339
+
340
+ Keep your backend and database canonical. Add Ablo as the shared write path for
341
+ the records that need multiplayer now and agent-safe writes later.
342
+
343
+ ```txt
344
+ Button
345
+ -> ablo.tasks.update(...)
346
+ -> Ablo
347
+ -> signed Data Source request
348
+ -> existing backend service
349
+ -> app database
350
+ -> Ablo realtime fanout
351
+ ```
352
+
353
+ The migration can be gradual:
354
+
355
+ 1. Declare schema for one resource, such as `tasks`.
356
+ 2. Keep existing server loads for first paint.
357
+ 3. Add `useAblo((ablo) => ablo.tasks.retrieve(id)) ?? serverTask` for live rows.
358
+ 4. Add one Data Source endpoint that calls the existing service layer.
359
+ 5. Move one mutation button from `fetch('/api/tasks/...')` to `ablo.tasks.update(...)`.
360
+ 6. Add an outbox/events path for writes that still happen outside Ablo.
361
+ 7. Let agents use the same `ablo.tasks.load(...)` and `ablo.tasks.update(...)`.
362
+
363
+ For the full Python shape, see
364
+ [Existing Python Backend](./examples/existing-python-backend.md).
365
+
366
+ ## 7. Data Source Endpoint
367
+
368
+ Use a Data Source when your app database remains the source of truth.
369
+
370
+ ```ts
371
+ // app/api/ablo/source/route.ts
372
+ import { dataSource } from '@ablo/sync-engine';
373
+ import { schema } from '@/ablo.schema';
374
+ import { db } from '@/db';
375
+
376
+ export const POST = dataSource({
377
+ schema,
378
+ signingSecret: process.env.ABLO_DATA_SOURCE_SIGNING_SECRET,
379
+
380
+ authorize() {
381
+ return { db };
382
+ },
383
+
384
+ async commit({ operations, clientTxId, context }) {
385
+ const rows = await context.auth.db.transaction(async (tx) => {
386
+ await tx.idempotency.upsert({ key: clientTxId });
387
+ return applyOperations(tx, operations);
388
+ });
389
+
390
+ return { rows };
391
+ },
392
+
393
+ tasks: {
394
+ async load({ id, context }) {
395
+ return context.auth.db.task.findUnique({ where: { id } });
396
+ },
397
+
398
+ async list({ query, context }) {
399
+ return context.auth.db.task.findMany({
400
+ take: query.limit ?? 100,
401
+ });
402
+ },
403
+ },
404
+ });
405
+ ```
406
+
407
+ Ablo gives you a Data Source URL, a signing secret, push/events URL, and status.
408
+ Your app stores:
409
+
410
+ ```bash
411
+ ABLO_DATA_SOURCE_SIGNING_SECRET=whsec_...
412
+ ```
413
+
414
+ The signing secret verifies Ablo's request. It is not a database credential.
415
+
416
+ ## 8. Agents
417
+
418
+ Agents should use the same model methods as the app when they can import the
419
+ schema.
420
+
421
+ ```ts
422
+ const [task] = await ablo.tasks.load({ where: { id: taskId } });
423
+ if (!task) return;
424
+
425
+ const intents = ablo.intents.list({ resource: 'tasks', id: taskId });
426
+ if (intents.length > 0) return { skipped: true, reason: 'busy' };
427
+
428
+ const snap = ablo.snapshot({ tasks: taskId });
429
+
430
+ await ablo.tasks.update(
431
+ taskId,
432
+ { status: 'done', summary: await summarize(task) },
433
+ { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
434
+ );
435
+ ```
436
+
437
+ Use AI SDK for the model loop. Put Ablo inside the tool that persists the final
438
+ change.
439
+
440
+ ```ts
441
+ const completeTask = tool({
442
+ description: 'Mark a task done with a summary',
443
+ inputSchema: z.object({
444
+ taskId: z.string(),
445
+ summary: z.string(),
446
+ }),
447
+ execute: async ({ taskId, summary }) => {
448
+ const snap = ablo.snapshot({ tasks: taskId });
449
+ return ablo.tasks.update(
450
+ taskId,
451
+ { status: 'done', summary },
452
+ { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
453
+ );
454
+ },
455
+ });
456
+ ```
457
+
458
+ Use schema-less `agent.run(...)`, `resource(...)`, and `commits.create(...)` only
459
+ for custom server runtimes that intentionally cannot import the schema.
460
+
461
+ ## Optional Surface
462
+
463
+ | Optional piece | Why it exists |
464
+ |---|---|
465
+ | `/react` | Live React selectors, provider lifecycle, presence, sync status. |
466
+ | `/testing` | Test harnesses and deterministic mocks. |
467
+ | `Data Source` | Keep your app database canonical. |
468
+ | `persistence: 'indexeddb'` | Durable browser cache and offline queueing for apps that need it. |
469
+ | `intents` | Show active work and coordinate before a write. |
470
+ | `snapshot` + `readAt` | Reject writes based on stale state. |
471
+ | `mutable`, `readOnly`, `field`, `indexed` | Advanced schema and local-cache tuning. |
472
+ | `resource(...)` and `commits.create(...)` | Low-level protocol access for custom runtimes. |
473
+
474
+ The first integration should not need most of these. Start with schema and
475
+ model methods, then add the optional pieces where the product actually needs
476
+ them.
477
+
478
+ ## Method Cheatsheet
479
+
480
+ | Method | Use it for |
481
+ |---|---|
482
+ | `load({ where })` | Async hydration from backing store/server. |
483
+ | `retrieve(id)` | Synchronous local read of one loaded row. |
484
+ | `list(options?)` | Synchronous local collection read. |
485
+ | `count(options?)` | Synchronous local count. |
486
+ | `create(data, options?)` | Create through the model resource. |
487
+ | `update(id, data, options?)` | Update through the model resource. |
488
+ | `delete(id, options?)` | Delete through the model resource. |
489
+ | `intents.list(target)` | See active work on a resource. |
490
+ | `intents.waitFor(target)` | Wait on the live intent stream. |
491
+
492
+ Do not use `ablo.commit` as the first write API. Most callers should never need
493
+ the low-level commit plane directly.
@@ -0,0 +1,140 @@
1
+ # Interaction Model
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:
12
+
13
+ ```
14
+ Capability -> Task -> Usage
15
+ ```
16
+
17
+ ## Primitives
18
+
19
+ | Primitive | Plane | Purpose |
20
+ |---|---|---|
21
+ | `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` | State | Pre-write coordination. It says what this actor is preparing to change. |
24
+ | `Commit` | Protocol | The durable write underneath model updates. Most users do not call it directly. |
25
+ | `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
+
33
+ ### Why each primitive is separate
34
+
35
+ The plane separation isn't ceremony — collapsing any two of these would
36
+ lose a property that's hard to recover later. A reader coming from
37
+ Replicache or Yjs would expect just `Commit`; here's what the others buy
38
+ you over that minimum:
39
+
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.
47
+ - **`Receipt` is not a `200 OK`.** It's the durable artifact a commit
48
+ produced — accepted commit id, server-assigned timestamps, stale-check
49
+ outcome — addressable after the fact and replayable into a different
50
+ client. A status code can't be re-read by a sub-agent that wasn't on
51
+ 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
+
65
+ 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`).
69
+
70
+ ## Run Loop
71
+
72
+ A normal schema-backed run is:
73
+
74
+ ```
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',
82
+ });
83
+ ```
84
+
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
+ ## Coordination
112
+
113
+ Intents broadcast across the org. When `agent:task-writer` declares an intent to
114
+ update a task, schema clients can see it through `ablo.intents.list(...)` or the
115
+ live intent stream. Callers decide whether to yield, wait, or fail fast.
116
+
117
+ ## Conflict resolution
118
+
119
+ Schema updates can carry `readAt` and `onStale`. If the state advanced past
120
+ `readAt`, Ablo applies the `onStale` policy:
121
+
122
+ - `reject` — fail the commit (first writer wins).
123
+ - `merge` — apply the write if it does not overlap with concurrent changes.
124
+ - `force` — apply the write unconditionally.
125
+
126
+ The choice is per-commit. No CRDT default; the policy is explicit.
127
+
128
+ ## Audit
129
+
130
+ Three tables observe the run:
131
+
132
+ - `agent_tasks` — one row per open/close cycle. Cost stats, prompt hash, capability id.
133
+ - `agent_actions_log` — one row per write, attributed to the task and the capability.
134
+ - `usage_event` — one row per accounted API call, attributed to the api key, the participant, and the task.
135
+
136
+ 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.
137
+
138
+ ## The contract in one sentence
139
+
140
+ Declare schema, load state, coordinate intent, update the model, and wait for confirmation.
@@ -0,0 +1,43 @@
1
+ # Claude Code
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ claude mcp add --transport http ablo-sync https://<your-app>/api/mcp
7
+ ```
8
+
9
+ That's it. The next `/help` in Claude Code will list the Ablo Sync tools.
10
+
11
+ ## With auth
12
+
13
+ If your deployment requires a capability 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 capability from the dashboard or via the API — see
21
+ [MCP overview](/docs/mcp#auth).
22
+
23
+ ## Verify
24
+
25
+ In Claude Code, run:
26
+
27
+ ```
28
+ /mcp list
29
+ ```
30
+
31
+ You should see `ablo-sync` with the resource tools enumerated.
32
+
33
+ ## Removing
34
+
35
+ ```bash
36
+ claude mcp remove ablo-sync
37
+ ```
38
+
39
+ ## More
40
+
41
+ - [MCP overview](/docs/mcp) — how the transport works.
42
+ - [Cursor setup](/docs/mcp/cursor) — same JSON, different UI.
43
+ - [Windsurf setup](/docs/mcp/windsurf) — same JSON, different UI.