@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,214 @@
1
+ # Connect Your Database
2
+
3
+ Every schema model has a backing store.
4
+
5
+ By default, Ablo stores the rows for the models you declare. That makes Ablo the
6
+ managed state store for those resources, the same way Stripe stores `Customer`
7
+ and `PaymentIntent` objects that you create through Stripe's API.
8
+
9
+ If you already have application tables and want those tables to remain
10
+ canonical, attach a Data Source. Then Ablo coordinates the write and calls your
11
+ app to commit it.
12
+
13
+ Use the SDK with an API key:
14
+
15
+ ```ts
16
+ import Ablo from '@ablo/sync-engine';
17
+ import { schema } from './ablo.schema';
18
+
19
+ export const ablo = Ablo({
20
+ schema,
21
+ apiKey: process.env.ABLO_API_KEY,
22
+ });
23
+ ```
24
+
25
+ Do not pass a database URL to `Ablo(...)`.
26
+
27
+ ## Backing Modes
28
+
29
+ | Mode | Where rows live | What `create/update/delete` does | Use when |
30
+ |---|---|---|---|
31
+ | Ablo-managed | Ablo | Writes directly to Ablo's managed state store, then returns the confirmed row and fans out realtime deltas. | New collaborative/agent state that can live in Ablo. |
32
+ | Data Source | Your app database | Sends a signed commit request to your route; your app writes its DB and returns canonical rows. | Existing app tables, regulated data, or teams that need their DB to stay canonical. |
33
+
34
+ The SDK call is the same in both modes:
35
+
36
+ ```ts
37
+ await ablo.tasks.create({ title: 'Draft launch plan', status: 'todo' });
38
+ await ablo.tasks.update('task_123', { status: 'done' });
39
+ const task = ablo.tasks.retrieve('task_123');
40
+ ```
41
+
42
+ Only the backing store changes.
43
+
44
+ Multiplayer behavior is the same in both modes. Writes made through
45
+ `ablo.<model>.create/update/delete` are coordinated by Ablo, then confirmed rows
46
+ fan out to subscribers. Direct database writes outside Ablo need Data Source
47
+ events so connected humans and agents see the change.
48
+
49
+ ## When To Use A Data Source
50
+
51
+ Use a Data Source only when your existing application database remains the
52
+ source of truth and Ablo should coordinate writes against it.
53
+
54
+ If you are migrating an app where every button already calls a backend endpoint,
55
+ read [Integration Guide](./integration-guide.md) first, then
56
+ [Existing Python Backend](./examples/existing-python-backend.md) for a concrete
57
+ service-owned database example.
58
+
59
+ ## What Ablo Gives You
60
+
61
+ When you add a Data Source in Ablo, you get:
62
+
63
+ | Field | Purpose |
64
+ |---|---|
65
+ | Data Source URL | The public HTTPS route in your app that Ablo will call. |
66
+ | Signing secret | Stored in your app as `ABLO_DATA_SOURCE_SIGNING_SECRET`; used to verify Ablo calls. |
67
+ | Push events URL | Ablo endpoint your app can call when rows change outside Ablo. |
68
+ | Status | Last successful request, last error, and delivery attempts. |
69
+
70
+ The shape is the same as a production webhook integration:
71
+
72
+ 1. Add a Data Source URL in Ablo.
73
+ 2. Store the signing secret in your app.
74
+ 3. Expose one signed HTTP route from your app.
75
+ 4. Keep your database credentials in your app.
76
+
77
+ ```bash
78
+ ABLO_DATA_SOURCE_SIGNING_SECRET=whsec_...
79
+ ```
80
+
81
+ ## Route
82
+
83
+ ```ts
84
+ // app/api/ablo/source/route.ts
85
+ import { dataSource } from '@ablo/sync-engine';
86
+ import { schema } from '@/ablo.schema';
87
+ import { db } from '@/db';
88
+
89
+ export const POST = dataSource({
90
+ schema,
91
+ signingSecret: process.env.ABLO_DATA_SOURCE_SIGNING_SECRET,
92
+
93
+ authorize() {
94
+ return { db };
95
+ },
96
+
97
+ async commit({ operations, clientTxId, context }) {
98
+ const rows = await context.auth.db.transaction(async (tx) => {
99
+ await tx.idempotency.upsert({ key: clientTxId, operations });
100
+ return applyOperations(tx, operations);
101
+ });
102
+
103
+ return { rows };
104
+ },
105
+
106
+ tasks: {
107
+ async load({ id, context }) {
108
+ return context.auth.db.task.findUnique({ where: { id } });
109
+ },
110
+
111
+ async list({ query, context }) {
112
+ return context.auth.db.task.findMany({
113
+ take: query.limit ?? 100,
114
+ });
115
+ },
116
+ },
117
+ });
118
+ ```
119
+
120
+ Your app code still writes through the normal model API:
121
+
122
+ ```ts
123
+ await ablo.tasks.update(
124
+ 'task_123',
125
+ { status: 'done' },
126
+ { wait: 'confirmed', readAt: snap.stamp, onStale: 'reject' },
127
+ );
128
+ ```
129
+
130
+ ## Commit Request
131
+
132
+ When Ablo calls your Data Source, it sends a signed JSON request:
133
+
134
+ ```ts
135
+ {
136
+ type: 'commit',
137
+ clientTxId: 'tx_...',
138
+ operations: [
139
+ {
140
+ type: 'UPDATE',
141
+ model: 'tasks',
142
+ id: 'task_123',
143
+ input: { status: 'done' },
144
+ readAt: 1042,
145
+ onStale: 'reject',
146
+ },
147
+ ],
148
+ scope: {
149
+ participantId: 'agent:triage',
150
+ participantKind: 'agent',
151
+ organizationId: 'org_123',
152
+ requiredSyncGroups: ['org:org_123'],
153
+ mode: 'live',
154
+ },
155
+ }
156
+ ```
157
+
158
+ Return canonical rows:
159
+
160
+ ```ts
161
+ {
162
+ rows: [
163
+ { id: 'task_123', title: 'Fix docs', status: 'done' },
164
+ ],
165
+ }
166
+ ```
167
+
168
+ Use explicit `deltas` only when your source already computes canonical change
169
+ events.
170
+
171
+ ## External Writes
172
+
173
+ If your app changes data outside Ablo, return those changes from an `events`
174
+ handler so connected humans and agents stay current:
175
+
176
+ ```ts
177
+ export const POST = dataSource({
178
+ schema,
179
+ signingSecret: process.env.ABLO_DATA_SOURCE_SIGNING_SECRET,
180
+
181
+ async events({ cursor, limit, context }) {
182
+ const page = await context.auth.db.outbox.after(cursor, { limit });
183
+
184
+ return {
185
+ events: page.rows.map((row) => ({
186
+ id: row.id,
187
+ model: row.model,
188
+ entityId: row.entityId,
189
+ type: row.type,
190
+ data: row.data,
191
+ organizationId: row.organizationId,
192
+ clientTxId: row.clientTxId,
193
+ occurredAt: row.createdAt.getTime(),
194
+ })),
195
+ nextCursor: page.nextCursor,
196
+ };
197
+ },
198
+ });
199
+ ```
200
+
201
+ `clientTxId` lets Ablo drop SDK echoes that already produced a realtime update.
202
+ Events without `clientTxId` are treated as external writes.
203
+
204
+ ## Security
205
+
206
+ - Verify requests with `ABLO_DATA_SOURCE_SIGNING_SECRET`.
207
+ - Keep database credentials in your app.
208
+ - Dedupe commits by `clientTxId`.
209
+ - Dedupe external events by event `id`.
210
+ - Use HTTPS in production.
211
+
212
+ The signing secret is not a database credential and does not give Ablo access to
213
+ your database. It only lets your route verify that the request came from Ablo
214
+ and was not modified in transit.
@@ -0,0 +1,84 @@
1
+ # Agent + Human
2
+
3
+ A task-writing agent that yields when a human is editing the same task.
4
+
5
+ ## Scenario
6
+
7
+ A product queue has tasks that humans and agents both update. They must not
8
+ collide:
9
+
10
+ - If the user is editing, the agent waits or yields.
11
+ - If the agent is updating, the UI can show who is active.
12
+ - If the task changes mid-run, the commit rejects instead of overwriting newer
13
+ state.
14
+
15
+ ## Schema-Backed Worker
16
+
17
+ Use the same schema client the app uses. The worker loads the task, checks active
18
+ intents, and writes through `ablo.tasks.update(...)`.
19
+
20
+ ```ts
21
+ import Ablo from '@ablo/sync-engine';
22
+ import { defineSchema, model, z } from '@ablo/sync-engine/schema';
23
+
24
+ const schema = defineSchema({
25
+ tasks: model({
26
+ title: z.string(),
27
+ status: z.enum(['todo', 'doing', 'done']),
28
+ }),
29
+ });
30
+
31
+ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
32
+
33
+ export async function markDone(taskId: string) {
34
+ await ablo.ready();
35
+
36
+ const [task] = await ablo.tasks.load({ where: { id: taskId } });
37
+ if (!task) return { status: 'not_found' };
38
+
39
+ const busy = ablo.intents.list({ resource: 'tasks', id: taskId });
40
+ if (busy.length > 0) return { status: 'busy', intents: busy };
41
+
42
+ const snap = ablo.snapshot({ tasks: taskId });
43
+ const updated = await ablo.tasks.update(
44
+ taskId,
45
+ { status: 'done' },
46
+ { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
47
+ );
48
+
49
+ return { status: 'done', task: updated };
50
+ }
51
+ ```
52
+
53
+ Advanced schema-less workers can use `Ablo({ apiKey }).agent(...)`, but that is
54
+ not the first integration path.
55
+
56
+ ## UI
57
+
58
+ ```tsx
59
+ 'use client';
60
+
61
+ import { useAblo } from '@ablo/sync-engine/react';
62
+
63
+ export function TaskRow({ task: serverTask }: Props) {
64
+ const data = useAblo((ablo) => ablo.tasks.retrieve(serverTask.id)) ?? serverTask;
65
+ const intents = useAblo((ablo) =>
66
+ ablo.intents.list({ resource: 'tasks', id: serverTask.id }),
67
+ ) ?? [];
68
+ const agentActive = intents.some((i) => i.participantKind === 'agent');
69
+
70
+ return (
71
+ <div>
72
+ <span>{data.title}</span>
73
+ {agentActive ? <span>Agent is updating...</span> : null}
74
+ </div>
75
+ );
76
+ }
77
+ ```
78
+
79
+ ## Why It Works
80
+
81
+ - Intents are visible on read and over the live stream.
82
+ - `ifBusy: 'wait'` lets agents wait for active work instead of racing.
83
+ - `readAt` plus `onStale: 'reject'` turns mid-flight changes into typed errors.
84
+ - Audit rows tie each accepted write back to the run that caused it.
@@ -0,0 +1,92 @@
1
+ # AI SDK Tool
2
+
3
+ Use AI SDK for the loop and Ablo for the state boundary inside the tool.
4
+
5
+ ```ts
6
+ import Ablo from '@ablo/sync-engine';
7
+ import { defineSchema, model, z as schemaZ } from '@ablo/sync-engine/schema';
8
+ import { streamText, tool } from 'ai';
9
+ import { z } from 'zod';
10
+
11
+ const schema = defineSchema({
12
+ tasks: model({
13
+ title: schemaZ.string(),
14
+ status: schemaZ.enum(['todo', 'doing', 'done']),
15
+ summary: schemaZ.string().optional(),
16
+ }),
17
+ });
18
+
19
+ const ablo = Ablo({
20
+ schema,
21
+ apiKey: process.env.ABLO_API_KEY,
22
+ });
23
+
24
+ const updateTask = tool({
25
+ description: 'Update a task in the product database.',
26
+ inputSchema: z.object({
27
+ taskId: z.string(),
28
+ status: z.enum(['todo', 'doing', 'done']).optional(),
29
+ summary: z.string().optional(),
30
+ }),
31
+ execute: async ({ taskId, status, summary }) => {
32
+ await ablo.ready();
33
+
34
+ const [task] = await ablo.tasks.load({ where: { id: taskId } });
35
+ if (!task) return { ok: false, reason: 'not_found' };
36
+
37
+ const busy = ablo.intents.list({ resource: 'tasks', id: taskId });
38
+ if (busy.length > 0) {
39
+ await ablo.intents.waitFor(
40
+ { resource: 'tasks', id: taskId },
41
+ { timeout: 30_000 },
42
+ );
43
+ }
44
+
45
+ const snap = ablo.snapshot({ tasks: taskId });
46
+ const intent = await ablo.intents.create({
47
+ target: { resource: 'tasks', id: taskId, field: 'status' },
48
+ action: 'update',
49
+ });
50
+
51
+ try {
52
+ const updated = await ablo.tasks.update(
53
+ taskId,
54
+ {
55
+ status: status ?? task.status,
56
+ summary: summary ?? task.summary,
57
+ },
58
+ {
59
+ intent,
60
+ readAt: snap.stamp,
61
+ onStale: 'reject',
62
+ wait: 'confirmed',
63
+ },
64
+ );
65
+
66
+ return { ok: true, task: updated };
67
+ } finally {
68
+ await intent.release();
69
+ }
70
+ },
71
+ });
72
+
73
+ export async function POST(req: Request) {
74
+ const { messages, model } = await req.json();
75
+
76
+ return streamText({
77
+ model,
78
+ messages,
79
+ tools: { updateTask },
80
+ }).toUIMessageStreamResponse();
81
+ }
82
+ ```
83
+
84
+ The important part is not the model provider. The important part is that the
85
+ tool:
86
+
87
+ - loads the latest task,
88
+ - checks active intent,
89
+ - writes with `readAt`,
90
+ - rejects stale state,
91
+ - waits for server confirmation.
92
+
@@ -0,0 +1,249 @@
1
+ # Existing Python Backend
2
+
3
+ Use this path when a product already has a Python API server and every button
4
+ currently calls an application endpoint.
5
+
6
+ The goal is not to replace the backend. Keep the Python service layer and
7
+ database as the source of truth. Add Ablo as the shared write path for records
8
+ that need multiplayer now and agent-safe writes later.
9
+
10
+ This also applies to any API-backed app, not only Python. A product like a YC
11
+ company's existing dashboard can keep its current endpoint/service/database
12
+ shape and migrate one coordinated resource at a time.
13
+
14
+ ```txt
15
+ Browser UI
16
+ -> Ablo model write
17
+ -> Python Data Source endpoint
18
+ -> existing Python service layer
19
+ -> app database
20
+ -> Ablo realtime fanout
21
+ -> browser UI and agents
22
+ ```
23
+
24
+ ## 1. Declare The Shared Models
25
+
26
+ Create a schema for the records that need realtime coordination.
27
+
28
+ ```ts
29
+ // web/ablo.schema.ts
30
+ import { defineSchema, model, z } from '@ablo/sync-engine/schema';
31
+
32
+ export const schema = defineSchema({
33
+ tasks: model({
34
+ id: z.string(),
35
+ title: z.string(),
36
+ status: z.enum(['todo', 'doing', 'done']),
37
+ updatedAt: z.string(),
38
+ }),
39
+ });
40
+ ```
41
+
42
+ ```ts
43
+ // web/ablo.ts
44
+ import Ablo from '@ablo/sync-engine';
45
+ import { schema } from './ablo.schema';
46
+
47
+ export const ablo = Ablo({
48
+ schema,
49
+ apiKey: process.env.ABLO_API_KEY,
50
+ });
51
+ ```
52
+
53
+ Mount the React provider near the app root so client components can subscribe to
54
+ model resources without importing server credentials.
55
+
56
+ ```tsx
57
+ // web/app/providers.tsx
58
+ 'use client';
59
+
60
+ import { AbloProvider } from '@ablo/sync-engine/react';
61
+ import { schema } from '@/ablo.schema';
62
+
63
+ export function Providers({ children }: { children: React.ReactNode }) {
64
+ return <AbloProvider schema={schema}>{children}</AbloProvider>;
65
+ }
66
+ ```
67
+
68
+ ## 2. Add Live Reads In The UI
69
+
70
+ Keep the first render backed by the existing Python endpoint. After that,
71
+ subscribe to the same model resource Ablo writes through.
72
+
73
+ ```tsx
74
+ 'use client';
75
+
76
+ import { useAblo } from '@ablo/sync-engine/react';
77
+
78
+ export function TaskRow({ task: serverTask }: { task: Task }) {
79
+ const task = useAblo((ablo) => ablo.tasks.retrieve(serverTask.id)) ?? serverTask;
80
+ const intents = useAblo((ablo) =>
81
+ ablo.intents.list({ resource: 'tasks', id: serverTask.id }),
82
+ ) ?? [];
83
+ const busy = intents.length > 0;
84
+
85
+ return (
86
+ <button disabled={busy || task.status === 'done'}>
87
+ {busy ? 'Someone is editing' : task.title}
88
+ </button>
89
+ );
90
+ }
91
+ ```
92
+
93
+ No string model key is needed in the first example. The selector reads from
94
+ `ablo.tasks`, so React uses the same model resource as writes and agents.
95
+
96
+ ## 3. Add One Python Data Source Endpoint
97
+
98
+ In Ablo, configure the Data Source URL:
99
+
100
+ ```txt
101
+ https://api.example.com/api/ablo/source
102
+ ```
103
+
104
+ Store the signing secret in the Python server:
105
+
106
+ ```bash
107
+ ABLO_DATA_SOURCE_SIGNING_SECRET=whsec_...
108
+ ```
109
+
110
+ Then expose one route that verifies the signed request and calls the existing
111
+ service functions.
112
+
113
+ ```py
114
+ # app/ablo_source.py
115
+ import base64
116
+ import hashlib
117
+ import hmac
118
+ import json
119
+ import os
120
+ import time
121
+ from fastapi import APIRouter, HTTPException, Request
122
+
123
+ from app.services.tasks import get_task, list_tasks, apply_task_operations
124
+
125
+ router = APIRouter()
126
+
127
+
128
+ def verify_ablo_signature(request: Request, raw_body: bytes) -> None:
129
+ secret = os.environ["ABLO_DATA_SOURCE_SIGNING_SECRET"].encode()
130
+ message_id = request.headers.get("webhook-id")
131
+ timestamp = request.headers.get("webhook-timestamp")
132
+ signature_header = request.headers.get("webhook-signature", "")
133
+
134
+ if not message_id or not timestamp or not signature_header:
135
+ raise HTTPException(status_code=401, detail="missing signature")
136
+
137
+ signed_at = int(timestamp)
138
+ if abs(int(time.time() * 1000) - signed_at) > 5 * 60 * 1000:
139
+ raise HTTPException(status_code=401, detail="expired signature")
140
+
141
+ payload = message_id.encode() + b"." + timestamp.encode() + b"." + raw_body
142
+ expected = base64.b64encode(
143
+ hmac.new(secret, payload, hashlib.sha256).digest()
144
+ ).decode()
145
+
146
+ presented = [
147
+ part.removeprefix("v1,")
148
+ for part in signature_header.split()
149
+ if part.startswith("v1,")
150
+ ]
151
+
152
+ if not any(hmac.compare_digest(expected, value) for value in presented):
153
+ raise HTTPException(status_code=401, detail="invalid signature")
154
+
155
+
156
+ @router.post("/api/ablo/source")
157
+ async def ablo_source(request: Request):
158
+ raw_body = await request.body()
159
+ verify_ablo_signature(request, raw_body)
160
+ body = json.loads(raw_body)
161
+
162
+ if body["type"] == "load":
163
+ if body["model"] == "tasks":
164
+ return {"row": await get_task(body["id"])}
165
+
166
+ if body["type"] == "list":
167
+ if body["model"] == "tasks":
168
+ return {"rows": await list_tasks(body.get("query", {}))}
169
+
170
+ if body["type"] == "commit":
171
+ rows = await apply_task_operations(
172
+ operations=body["operations"],
173
+ client_tx_id=body.get("clientTxId"),
174
+ scope=body.get("scope", {}),
175
+ )
176
+ return {"rows": rows}
177
+
178
+ raise HTTPException(status_code=400, detail="unsupported request")
179
+ ```
180
+
181
+ `apply_task_operations` should reuse the same transaction and validation logic
182
+ the existing Python endpoints already use. Dedupe by `clientTxId` so retries are
183
+ safe.
184
+
185
+ ## 4. Move Buttons Gradually
186
+
187
+ Existing button path:
188
+
189
+ ```txt
190
+ Button -> Python endpoint -> service -> database
191
+ ```
192
+
193
+ Target button path:
194
+
195
+ ```txt
196
+ Button -> ablo.tasks.update(...)
197
+ Ablo -> Python Data Source endpoint
198
+ Python service -> database
199
+ Ablo -> realtime fanout and receipt
200
+ ```
201
+
202
+ The app does not need a flag-day rewrite. Move one resource at a time.
203
+
204
+ ```ts
205
+ const snap = ablo.snapshot({ tasks: taskId });
206
+
207
+ await ablo.tasks.update(
208
+ taskId,
209
+ { status: 'done' },
210
+ { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
211
+ );
212
+ ```
213
+
214
+ Use `readAt` and `onStale: 'reject'` for actions that depend on state the user
215
+ or agent already saw.
216
+
217
+ ## 5. Report Direct Database Writes
218
+
219
+ Some writes will still happen through old Python endpoints, cron jobs, admin
220
+ tools, or imports. Those bypass Ablo until the backend reports them.
221
+
222
+ Add an outbox table in Python and expose it through Data Source `events`:
223
+
224
+ ```txt
225
+ old Python endpoint -> service -> database -> outbox row
226
+ Ablo polls events -> realtime fanout
227
+ ```
228
+
229
+ Each event needs a stable event id, model name, entity id, event type, row data,
230
+ and timestamp. If the change originated from an Ablo commit, include the same
231
+ `clientTxId` so Ablo can ignore its own echo.
232
+
233
+ ## 6. Add Agents Later
234
+
235
+ Agents use the same model API as the UI:
236
+
237
+ ```ts
238
+ const [task] = await ablo.tasks.load({ where: { id: taskId } });
239
+ const snap = ablo.snapshot({ tasks: taskId });
240
+
241
+ await ablo.tasks.update(
242
+ taskId,
243
+ { status: 'done' },
244
+ { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
245
+ );
246
+ ```
247
+
248
+ That is the point of the migration: humans and agents share one write contract,
249
+ while the Python backend remains the canonical business logic and database owner.