@abloatai/ablo 0.7.0 → 0.9.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 (181) hide show
  1. package/CHANGELOG.md +72 -1
  2. package/README.md +80 -66
  3. package/dist/BaseSyncedStore.d.ts +73 -0
  4. package/dist/BaseSyncedStore.js +179 -5
  5. package/dist/Model.d.ts +42 -0
  6. package/dist/Model.js +103 -44
  7. package/dist/SyncEngineContext.d.ts +2 -1
  8. package/dist/SyncEngineContext.js +5 -3
  9. package/dist/agent/session.js +6 -5
  10. package/dist/ai-sdk/coordination-context.js +4 -0
  11. package/dist/ai-sdk/index.d.ts +56 -47
  12. package/dist/ai-sdk/index.js +56 -47
  13. package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
  14. package/dist/ai-sdk/intent-broadcast.js +11 -4
  15. package/dist/ai-sdk/wrap.d.ts +14 -11
  16. package/dist/ai-sdk/wrap.js +11 -13
  17. package/dist/auth/credentialSource.d.ts +34 -0
  18. package/dist/auth/credentialSource.js +63 -0
  19. package/dist/auth/index.d.ts +2 -22
  20. package/dist/auth/index.js +26 -36
  21. package/dist/auth/schemas.d.ts +35 -0
  22. package/dist/auth/schemas.js +53 -0
  23. package/dist/client/Ablo.d.ts +259 -33
  24. package/dist/client/Ablo.js +276 -73
  25. package/dist/client/ApiClient.d.ts +52 -4
  26. package/dist/client/ApiClient.js +236 -66
  27. package/dist/client/auth.d.ts +21 -2
  28. package/dist/client/auth.js +77 -5
  29. package/dist/client/createInternalComponents.d.ts +2 -0
  30. package/dist/client/createInternalComponents.js +8 -1
  31. package/dist/client/createModelProxy.d.ts +187 -79
  32. package/dist/client/createModelProxy.js +203 -68
  33. package/dist/client/httpClient.d.ts +71 -0
  34. package/dist/client/httpClient.js +69 -0
  35. package/dist/client/identity.d.ts +2 -6
  36. package/dist/client/identity.js +63 -11
  37. package/dist/client/index.d.ts +1 -0
  38. package/dist/client/index.js +1 -0
  39. package/dist/client/registerDataSource.d.ts +19 -0
  40. package/dist/client/registerDataSource.js +59 -0
  41. package/dist/client/validateAbloOptions.d.ts +2 -1
  42. package/dist/client/validateAbloOptions.js +8 -7
  43. package/dist/core/DatabaseManager.js +30 -2
  44. package/dist/core/openIDBWithTimeout.d.ts +36 -0
  45. package/dist/core/openIDBWithTimeout.js +88 -1
  46. package/dist/errorCodes.d.ts +92 -1
  47. package/dist/errorCodes.js +139 -7
  48. package/dist/errors.d.ts +54 -3
  49. package/dist/errors.js +192 -44
  50. package/dist/index.d.ts +23 -10
  51. package/dist/index.js +21 -8
  52. package/dist/keys/index.d.ts +76 -0
  53. package/dist/keys/index.js +171 -0
  54. package/dist/mutators/UndoManager.d.ts +86 -50
  55. package/dist/mutators/UndoManager.js +129 -22
  56. package/dist/mutators/inverseOp.d.ts +129 -0
  57. package/dist/mutators/inverseOp.js +74 -0
  58. package/dist/mutators/readerActions.d.ts +1 -1
  59. package/dist/mutators/undoApply.d.ts +42 -0
  60. package/dist/mutators/undoApply.js +143 -0
  61. package/dist/query/client.d.ts +10 -9
  62. package/dist/query/client.js +22 -14
  63. package/dist/react/AbloProvider.d.ts +23 -101
  64. package/dist/react/AbloProvider.js +61 -103
  65. package/dist/react/ClientSideSuspense.d.ts +1 -1
  66. package/dist/react/DefaultFallback.d.ts +1 -1
  67. package/dist/react/SyncGroupProvider.d.ts +1 -1
  68. package/dist/react/index.d.ts +3 -2
  69. package/dist/react/index.js +3 -2
  70. package/dist/react/useAblo.d.ts +4 -4
  71. package/dist/react/useAblo.js +10 -5
  72. package/dist/react/useCurrentUserId.d.ts +1 -1
  73. package/dist/react/useCurrentUserId.js +1 -1
  74. package/dist/react/useMutators.js +19 -12
  75. package/dist/react/useReactive.js +16 -3
  76. package/dist/schema/ddl.d.ts +26 -3
  77. package/dist/schema/ddl.js +152 -4
  78. package/dist/schema/index.d.ts +4 -0
  79. package/dist/schema/index.js +12 -0
  80. package/dist/schema/model.d.ts +11 -0
  81. package/dist/schema/model.js +2 -0
  82. package/dist/schema/openapi.d.ts +28 -0
  83. package/dist/schema/openapi.js +118 -0
  84. package/dist/schema/plane.d.ts +23 -0
  85. package/dist/schema/plane.js +19 -0
  86. package/dist/schema/relation.d.ts +20 -0
  87. package/dist/schema/serialize.d.ts +7 -3
  88. package/dist/schema/serialize.js +6 -2
  89. package/dist/schema/sync-delta-row.d.ts +157 -0
  90. package/dist/schema/sync-delta-row.js +102 -0
  91. package/dist/schema/sync-delta-wire.d.ts +180 -0
  92. package/dist/schema/sync-delta-wire.js +102 -0
  93. package/dist/server/adapter.d.ts +156 -0
  94. package/dist/server/adapter.js +19 -0
  95. package/dist/server/commit.d.ts +82 -0
  96. package/dist/server/commit.js +1 -0
  97. package/dist/server/index.d.ts +14 -0
  98. package/dist/server/index.js +1 -0
  99. package/dist/server/next.d.ts +51 -0
  100. package/dist/server/next.js +47 -0
  101. package/dist/server/read-config.d.ts +60 -0
  102. package/dist/server/read-config.js +8 -0
  103. package/dist/server/storage-mode.d.ts +17 -0
  104. package/dist/server/storage-mode.js +12 -0
  105. package/dist/source/adapter.d.ts +59 -0
  106. package/dist/source/adapter.js +19 -0
  107. package/dist/source/adapters/drizzle.d.ts +34 -0
  108. package/dist/source/adapters/drizzle.js +147 -0
  109. package/dist/source/adapters/memory.d.ts +12 -0
  110. package/dist/source/adapters/memory.js +114 -0
  111. package/dist/source/adapters/prisma.d.ts +57 -0
  112. package/dist/source/adapters/prisma.js +199 -0
  113. package/dist/source/conformance.d.ts +32 -0
  114. package/dist/source/conformance.js +134 -0
  115. package/dist/source/contract.d.ts +143 -0
  116. package/dist/source/contract.js +98 -0
  117. package/dist/source/index.d.ts +61 -10
  118. package/dist/source/index.js +98 -0
  119. package/dist/source/next.d.ts +33 -0
  120. package/dist/source/next.js +26 -0
  121. package/dist/sync/BootstrapHelper.d.ts +10 -0
  122. package/dist/sync/BootstrapHelper.js +56 -42
  123. package/dist/sync/ConnectionManager.d.ts +57 -1
  124. package/dist/sync/ConnectionManager.js +186 -11
  125. package/dist/sync/HydrationCoordinator.d.ts +93 -17
  126. package/dist/sync/HydrationCoordinator.js +241 -41
  127. package/dist/sync/NetworkProbe.d.ts +60 -18
  128. package/dist/sync/NetworkProbe.js +121 -23
  129. package/dist/sync/SyncWebSocket.d.ts +45 -70
  130. package/dist/sync/SyncWebSocket.js +113 -89
  131. package/dist/sync/createIntentStream.js +10 -1
  132. package/dist/sync/participants.js +5 -2
  133. package/dist/transactions/TransactionQueue.js +13 -1
  134. package/dist/types/streams.d.ts +9 -0
  135. package/dist/utils/mobx-setup.js +1 -0
  136. package/dist/webhooks/events.d.ts +38 -0
  137. package/dist/webhooks/events.js +40 -0
  138. package/dist/webhooks/index.d.ts +10 -0
  139. package/dist/webhooks/index.js +10 -0
  140. package/dist/wire/errorEnvelope.d.ts +34 -0
  141. package/dist/wire/errorEnvelope.js +86 -0
  142. package/dist/wire/frames.d.ts +119 -0
  143. package/dist/wire/frames.js +1 -0
  144. package/dist/wire/index.d.ts +24 -0
  145. package/dist/wire/index.js +21 -0
  146. package/dist/wire/listEnvelope.d.ts +45 -0
  147. package/dist/wire/listEnvelope.js +17 -0
  148. package/docs/api-keys.md +5 -5
  149. package/docs/api.md +125 -65
  150. package/docs/audit.md +16 -9
  151. package/docs/cli.md +57 -47
  152. package/docs/client-behavior.md +54 -40
  153. package/docs/coordination.md +66 -80
  154. package/docs/data-sources.md +56 -34
  155. package/docs/examples/agent-human.md +74 -28
  156. package/docs/examples/ai-sdk-tool.md +29 -22
  157. package/docs/examples/existing-python-backend.md +41 -26
  158. package/docs/examples/nextjs.md +32 -17
  159. package/docs/examples/scoped-agent.md +43 -28
  160. package/docs/examples/server-agent.md +40 -15
  161. package/docs/guarantees.md +38 -27
  162. package/docs/identity.md +65 -59
  163. package/docs/index.md +30 -19
  164. package/docs/integration-guide.md +78 -78
  165. package/docs/interaction-model.md +43 -35
  166. package/docs/mcp/claude-code.md +11 -19
  167. package/docs/mcp/cursor.md +7 -25
  168. package/docs/mcp/windsurf.md +7 -20
  169. package/docs/mcp.md +103 -26
  170. package/docs/quickstart.md +63 -61
  171. package/docs/react.md +24 -16
  172. package/docs/roadmap.md +13 -13
  173. package/docs/schema-contract.md +111 -0
  174. package/docs/the-loop.md +21 -0
  175. package/examples/README.md +8 -4
  176. package/examples/data-source/README.md +10 -7
  177. package/examples/data-source/customer-server.ts +27 -25
  178. package/examples/data-source/run.ts +4 -3
  179. package/examples/quickstart.ts +1 -1
  180. package/llms.txt +55 -21
  181. package/package.json +48 -3
@@ -0,0 +1,111 @@
1
+ # Schema Contract
2
+
3
+ Ablo's schema is the integration contract. Define it once, pass it to `Ablo(...)`,
4
+ and every actor gets the same typed model surface:
5
+
6
+ ```txt
7
+ defineSchema(...) -> ablo.<model>.create/retrieve/update/claim(...)
8
+ ```
9
+
10
+ That one object drives:
11
+
12
+ - typed model clients in trusted server runtimes,
13
+ - React selectors through `useAblo((ablo) => ablo.<model>.get(id))`,
14
+ - agent and background-worker writes,
15
+ - Data Source request/response shape when your database stays canonical,
16
+ - hosted schema push, migration planning, and schema-version gating.
17
+
18
+ ## Minimal shape
19
+
20
+ ```ts
21
+ import Ablo from '@abloatai/ablo';
22
+ import { defineSchema, model, z } from '@abloatai/ablo/schema';
23
+
24
+ export const schema = defineSchema({
25
+ weatherReports: model({
26
+ location: z.string(),
27
+ status: z.enum(['pending', 'ready']),
28
+ forecast: z.string().optional(),
29
+ }),
30
+ });
31
+
32
+ export const ablo = Ablo({
33
+ schema,
34
+ apiKey: process.env.ABLO_API_KEY,
35
+ });
36
+
37
+ await ablo.ready();
38
+
39
+ const report = await ablo.weatherReports.create({
40
+ data: {
41
+ location: 'Stockholm',
42
+ status: 'pending',
43
+ },
44
+ });
45
+ ```
46
+
47
+ The model key (`weatherReports`) becomes the client namespace
48
+ (`ablo.weatherReports`). The Zod fields become the create/update/read type
49
+ contract. You should not create a parallel string-keyed write path for the same
50
+ data.
51
+
52
+ ## Reads and writes
53
+
54
+ Use async reads when the row may not be local:
55
+
56
+ ```ts
57
+ const report = await ablo.weatherReports.retrieve({ id: reportId });
58
+ const ready = await ablo.weatherReports.list({ where: { status: 'ready' } });
59
+ ```
60
+
61
+ Use synchronous local reads in render after data has synced:
62
+
63
+ ```ts
64
+ const report = ablo.weatherReports.get(reportId);
65
+ const pending = ablo.weatherReports.getAll({ where: { status: 'pending' } });
66
+ ```
67
+
68
+ Use model writes for every actor:
69
+
70
+ ```ts
71
+ await ablo.weatherReports.update({ id: reportId, data: { status: 'ready' }, wait: 'confirmed' });
72
+ ```
73
+
74
+ ## Coordination
75
+
76
+ Agents and background jobs often read, call a tool or model, then write later.
77
+ Wrap that slow span in `claim`:
78
+
79
+ ```ts
80
+ const handle = await ablo.weatherReports.claim({ id: reportId });
81
+ const forecast = await getForecast(handle.data.location);
82
+ await ablo.weatherReports.update({ id: handle.data.id, data: { status: 'ready', forecast } });
83
+ await handle.release();
84
+ ```
85
+
86
+ If another writer already holds the row, `claim` waits, re-reads, and hands you
87
+ the fresh row. Reads stay open; only acting on the row serializes.
88
+
89
+ ## Storage boundary
90
+
91
+ Every schema model needs a backing store:
92
+
93
+ - Use Ablo-managed state when the row can live in Ablo.
94
+ - Use a Data Source when your app database remains canonical.
95
+
96
+ Do not pass a database URL to `Ablo(...)`. Trusted runtimes use `ABLO_API_KEY`.
97
+ Browser code goes through `<AbloProvider>` or a scoped session route, never a raw
98
+ API key.
99
+
100
+ ## Rules of thumb
101
+
102
+ - Start with fields and relations before load/index tuning.
103
+ - Import one schema into app code, server actions, agents, and Data Source routes.
104
+ - Keep direct database writes out of the coordinated path unless they are reported
105
+ back through Data Source events.
106
+ - Use `claim` for slow read -> think -> write spans.
107
+ - Use `readAt` + `onStale: 'reject'` when a write must fail if the row changed
108
+ after it was read.
109
+
110
+ For the shortest runnable path, start with [Quickstart](./quickstart.md). For a
111
+ production app, continue with [Integration Guide](./integration-guide.md).
@@ -0,0 +1,21 @@
1
+ # The loop: how your data flows
2
+
3
+ This explainer moved to the canonical, maintained docs:
4
+
5
+ **→ https://abloatai.com/docs/webhooks**
6
+
7
+ The short version: Ablo has the same two-sided shape as Stripe — **you call Ablo to make changes (the client), and Ablo calls you to persist them (a signed webhook)** — plus realtime sync to every connected client.
8
+
9
+ ```
10
+ your app ──write──▶ Ablo (hosted) ──realtime sync──▶ other clients
11
+ (the client) the transaction log (live, optimistic)
12
+
13
+ └──signed event──▶ /api/ablo/[...all] ──▶ YOUR database
14
+ (the webhook route)
15
+ ```
16
+
17
+ Ablo owns the ordered transaction log (the source of truth); your database is a
18
+ materialized copy you keep via the webhook. See the link above for the full
19
+ guide: scaffolding the handler (`ablo init`), local testing (`ablo dev`),
20
+ registering an endpoint (`ablo webhooks create`), signature verification, the
21
+ delivery/retry model, and best practices.
@@ -31,9 +31,13 @@ intentionally cannot import the app schema.
31
31
 
32
32
  ## Running
33
33
 
34
+ Run from the package root, not `examples/` — the `examples/` folder has
35
+ no `package.json`, so Node resolves the entry path against the package
36
+ root and a bare `quickstart.ts` won't be found.
37
+
34
38
  ```bash
35
- cd packages/sync-engine/examples
36
- ABLO_API_KEY=sk_test_... npx tsx quickstart.ts
39
+ cd packages/sync-engine
40
+ ABLO_API_KEY=sk_test_... npx tsx examples/quickstart.ts
37
41
  ```
38
42
 
39
43
  ## Data Source (customer-owned database)
@@ -45,8 +49,8 @@ orchestrator drives the customer handler in-process so signer and
45
49
  verifier exchange real signed bytes without leaving the process.
46
50
 
47
51
  ```bash
48
- cd packages/sync-engine/examples
49
- npx tsx data-source/run.ts
52
+ cd packages/sync-engine
53
+ npx tsx examples/data-source/run.ts
50
54
  ```
51
55
 
52
56
  See `data-source/README.md` for what each file teaches and the
@@ -16,10 +16,14 @@ when they want Ablo to coordinate writes against rows stored in
16
16
  ## Run
17
17
 
18
18
  ```bash
19
- cd packages/sync-engine/examples
20
- npx tsx data-source/run.ts
19
+ cd packages/sync-engine
20
+ npx tsx examples/data-source/run.ts
21
21
  ```
22
22
 
23
+ Run from the package root, not `examples/`: the `examples/` folder has
24
+ no `package.json`, so Node resolves the entry path against the package
25
+ root and a bare `data-source/run.ts` won't be found.
26
+
23
27
  No network port, no env vars, no cloud credentials. The orchestrator
24
28
  calls the handler in-process. Signer and verifier still exchange
25
29
  signed bytes — flip the API key and you'll see a 401.
@@ -34,9 +38,9 @@ signed bytes — flip the API key and you'll see a 401.
34
38
  3. **The customer DB stays canonical.** Ablo never sees rows
35
39
  directly; it only sees the response payload from the customer's
36
40
  handler.
37
- 4. **The outbox feed.** Writes that bypass Ablo (cron, dashboards,
38
- batch imports) show up on the next `events` poll so Ablo can fan
39
- them out to connected clients.
41
+ 4. **The outbox feed.** Every committed app-row change gets an outbox marker.
42
+ Ablo filters markers for commits it already appended and uses the same feed
43
+ to repair a failed post-commit append.
40
44
 
41
45
  ## Production wiring
42
46
 
@@ -92,8 +96,7 @@ data layer. The handler shape stays the same:
92
96
  - `tasks.load({ id })` -> `db.task.findUnique({ where: { id } })`
93
97
  - `tasks.list({ query })` -> `db.task.findMany({ take, cursor })`
94
98
  - `tasks.commit({ operations, clientTxId })` -> `db.$transaction` that
95
- applies each `op` and tags the row with `clientTxId` for idempotent
96
- retries
99
+ applies each `op` and writes an outbox marker with `clientTxId` before commit
97
100
  - `events({ cursor, limit })` -> read from your outbox table, return
98
101
  rows with their `clientTxId` (Ablo dedupes its own commits) and the
99
102
  resume cursor
@@ -15,7 +15,7 @@
15
15
  * inside a transaction. The shape of the handlers stays identical.
16
16
  */
17
17
 
18
- import Ablo, { dataSource } from '@abloatai/ablo';
18
+ import Ablo, { dataSource, sourceEventForOperation } from '@abloatai/ablo';
19
19
  import { schema } from './schema';
20
20
 
21
21
  type TaskRow = {
@@ -29,16 +29,10 @@ type TaskRow = {
29
29
  const taskStore = new Map<string, TaskRow>();
30
30
 
31
31
  // Outbox table. In production this is a `tasks_outbox` Postgres table
32
- // populated by triggers or service code. Ablo polls `events` to fan
33
- // out changes that didn't originate from an Ablo commit.
34
- type OutboxRow = {
35
- id: string;
36
- entityId: string;
37
- type: Ablo.Source.Operation['type'];
38
- data: TaskRow | null;
39
- clientTxId?: string;
40
- };
41
- const outbox: OutboxRow[] = [];
32
+ // populated in the same transaction as the app-row write. Ablo polls `events`
33
+ // to fan out changes that bypassed Ablo, and to repair SDK-origin writes if
34
+ // Ablo's immediate post-commit append failed.
35
+ const outbox: Ablo.Source.Event[] = [];
42
36
  let outboxSequence = 0;
43
37
 
44
38
  // Seed one row so the example's first `load` returns something.
@@ -132,19 +126,14 @@ export const handleAbloSource = dataSource({
132
126
  const start = cursor ? Number(cursor) : 0;
133
127
  const cap = limit ?? 100;
134
128
  const slice = outbox.slice(start, start + cap);
135
- const events = slice.map((row) => ({
136
- id: row.id,
137
- model: 'tasks',
138
- entityId: row.entityId,
139
- type: row.type,
140
- data: row.data,
141
- ...(row.clientTxId ? { clientTxId: row.clientTxId } : {}),
142
- }));
143
129
  const nextCursor =
144
130
  start + slice.length < outbox.length
145
131
  ? String(start + slice.length)
146
132
  : undefined;
147
- return { events, ...(nextCursor !== undefined ? { nextCursor } : {}) };
133
+ return {
134
+ events: slice,
135
+ ...(nextCursor !== undefined ? { nextCursor } : {}),
136
+ };
148
137
  },
149
138
  });
150
139
 
@@ -166,7 +155,7 @@ function applyOperation(
166
155
  : {}),
167
156
  };
168
157
  taskStore.set(id, row);
169
- appendOutbox({ entityId: id, type: 'CREATE', data: row, clientTxId });
158
+ appendOutbox({ operation: op, entityId: id, data: row, clientTxId });
170
159
  return row;
171
160
  }
172
161
 
@@ -175,7 +164,7 @@ function applyOperation(
175
164
  if (!existing) return null;
176
165
  const next: TaskRow = { ...existing, ...(op.input as Partial<TaskRow>) };
177
166
  taskStore.set(id, next);
178
- appendOutbox({ entityId: id, type: 'UPDATE', data: next, clientTxId });
167
+ appendOutbox({ operation: op, entityId: id, data: next, clientTxId });
179
168
  return next;
180
169
  }
181
170
 
@@ -183,16 +172,29 @@ function applyOperation(
183
172
  const existing = taskStore.get(id);
184
173
  if (!existing) return null;
185
174
  taskStore.delete(id);
186
- appendOutbox({ entityId: id, type: 'DELETE', data: null, clientTxId });
175
+ appendOutbox({ operation: op, entityId: id, data: null, clientTxId });
187
176
  return existing;
188
177
  }
189
178
 
190
179
  return null;
191
180
  }
192
181
 
193
- function appendOutbox(input: Omit<OutboxRow, 'id'>): void {
182
+ function appendOutbox(input: {
183
+ operation: Ablo.Source.Operation;
184
+ entityId: string;
185
+ data: TaskRow | null;
186
+ clientTxId: string | undefined;
187
+ }): void {
194
188
  outboxSequence += 1;
195
- outbox.push({ id: `evt_${outboxSequence}`, ...input });
189
+ outbox.push(
190
+ sourceEventForOperation({
191
+ eventId: `evt_${outboxSequence}`,
192
+ operation: input.operation,
193
+ entityId: input.entityId,
194
+ data: input.data,
195
+ ...(input.clientTxId ? { clientTxId: input.clientTxId } : {}),
196
+ }),
197
+ );
196
198
  }
197
199
 
198
200
  // Exposed for the orchestrator's `run.ts`. A real customer doesn't
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * End-to-end Data Source demo.
3
3
  *
4
- * Run:
4
+ * Run (from the package root — the examples/ folder has no package.json
5
+ * of its own, so Node resolves module paths against the package root):
5
6
  *
6
- * cd packages/sync-engine/examples
7
- * npx tsx data-source/run.ts
7
+ * cd packages/sync-engine
8
+ * npx tsx examples/data-source/run.ts
8
9
  *
9
10
  * What this proves:
10
11
  *
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Run:
5
5
  *
6
- * ABLO_API_KEY=sk_test_... npx tsx quickstart.ts
6
+ * ABLO_API_KEY=sk_test_... npx tsx examples/quickstart.ts
7
7
  */
8
8
 
9
9
  import Ablo from '@abloatai/ablo';
package/llms.txt CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Ablo is the state coordination layer for apps where humans and agents edit the same data.
4
4
 
5
+ Here is the problem it solves. Two writers touch `report_stockholm` at once. The agent claims the row, does slow work (an LLM call, a fetch), and commits; the human's UI sees the claim live and never clobbers it. Claims don't lock. If another writer holds the row, `claim` waits for them, re-reads the fresh row, then hands it to you — so two writers serialize instead of clobbering.
6
+
5
7
  Use AI SDK for the agent loop. Use Ablo when agent reads and writes must persist, coordinate with concurrent work, and leave an audit trail.
6
8
 
7
9
  ## Use this API
@@ -21,7 +23,7 @@ const schema = defineSchema({
21
23
 
22
24
  const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
23
25
 
24
- const [report] = await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
26
+ const report = await ablo.weatherReports.retrieve('report_stockholm');
25
27
  if (!report) throw new Error('Row not found');
26
28
 
27
29
  const updated = await ablo.weatherReports.claim('report_stockholm', async (report) => {
@@ -33,20 +35,29 @@ const updated = await ablo.weatherReports.claim('report_stockholm', async (repor
33
35
  });
34
36
  ```
35
37
 
36
- That is the normal app path: declare models in a schema, then use `ablo.<model>.load(...)`, `ablo.<model>.retrieve(...)`, `ablo.<model>.create(...)`, `ablo.<model>.update(...)`, and `ablo.<model>.delete(...)`.
38
+ That is the normal app path: declare models in a schema, then use `ablo.<model>.retrieve(...)`, `ablo.<model>.create(...)`, `ablo.<model>.update(...)`, and `ablo.<model>.delete(...)`.
39
+
40
+ Treat the schema as the integration contract. It drives typed model clients,
41
+ React selectors, server and agent writes, Data Source request/response shape,
42
+ hosted schema push, and schema-version gating. Do not invent a parallel
43
+ string-keyed write path for rows that belong to a schema model.
37
44
 
38
45
  For full integrations, use `integration-guide` as the canonical doc. It covers
39
46
  the same model API across Ablo-managed state, Data Source-backed app databases,
40
47
  React selectors, multiplayer, and future agent workers.
41
48
 
42
- `ablo.<model>.list(...)` and `count(...)` are synchronous local reads. They
43
- accept `where`, `filter`, `orderBy`, `limit`, `offset`, and `scope`; scope
44
- defaults to `'live'`, with `'archived'` and `'all'` for lifecycle-aware reads.
49
+ Reads come in two flavors, and you pick by whether you can wait. `retrieve(id)`
50
+ (one row) and `list({ where })` (many) are async — they hit the server and return
51
+ a Promise, so await them. `get(id)`, `getAll({ where })`, and `getCount({ where })`
52
+ are synchronous — they read the local graph and are reactive in render, so no
53
+ await. The query reads accept `where`, `filter`, `orderBy`, `limit`, `offset`,
54
+ and `state`; state defaults to `'live'`, with `'archived'` and `'all'` to include
55
+ retired rows.
45
56
 
46
- Advanced schema-less agents exist for workers that cannot import the app schema,
47
- but do not teach that path first.
57
+ Workers that can't import the app schema can use a schema-less mode (covered in
58
+ `integration-guide`).
48
59
 
49
- React reads should use selector `useAblo`: `useAblo((ablo) => ablo.weatherReports.retrieve(id))`.
60
+ React reads should use selector `useAblo`: `useAblo((ablo) => ablo.weatherReports.get(id))` (synchronous local read, reactive in render).
50
61
  Use zero-argument `useAblo()` only when a component needs the client for an
51
62
  event handler or effect. Treat `useQuery`, `useOne`, `useReader`, and
52
63
  `useMutate` as compatibility hooks for older string-keyed integrations, not the
@@ -57,7 +68,7 @@ first integration path.
57
68
  Multiplayer is not a separate mode. When human UI, server actions, and agents use
58
69
  the same schema client and write through `ablo.<model>`, Ablo coordinates the
59
70
  shared model stream: confirmed deltas fan out to subscribers, active claims are
60
- visible through `claimState(id)`, and stale writes can be rejected with `readAt`.
71
+ visible through `claim.state(id)`, and stale writes can be rejected with `readAt`.
61
72
 
62
73
  If an app writes directly to its own database outside Ablo, that write bypasses
63
74
  coordination until the app reports it through Data Source events.
@@ -65,7 +76,7 @@ coordination until the app reports it through Data Source events.
65
76
  ## Nouns
66
77
 
67
78
  - `Model client` is the typed `ablo.<model>` object generated from schema.
68
- - `Claim` holds a model row while slow work runs; `claimState(id)` observes it.
79
+ - `Claim` holds a model row while slow work runs; `claim.state(id)` observes it.
69
80
  - `Commit` is the durable protocol write behind `ablo.<model>.update(...)`.
70
81
  - `Receipt` confirms the commit.
71
82
 
@@ -76,36 +87,46 @@ Server reads through `ablo.model(name)` can pass `ifClaimed: 'return'` to
76
87
  receive active claims, `ifClaimed: 'fail'` to throw `AbloClaimedError`, or
77
88
  `ifClaimed: 'wait'` to wait until the active claim clears.
78
89
 
79
- Schema clients wait from the realtime claim stream. Schema-less HTTP callers must provide an explicit `claimedPollInterval` when using `ifClaimed: 'wait'`; Ablo does not hide a hard-coded polling loop.
90
+ Schema clients learn when a claim clears by listening to the live claim stream, so they don't need to poll. Schema-less HTTP callers must provide an explicit `claimedPollInterval` when using `ifClaimed: 'wait'`; Ablo does not hide a hard-coded polling loop.
80
91
 
81
92
  Use `claimedTimeout` only as a maximum wait, not as the coordination mechanism.
82
93
 
83
94
  ## Guarantees
84
95
 
85
- `wait: 'confirmed'` means the server accepted the write. Schema model writes are optimistic by default; server rejection rolls back local state. Use `snapshot(...)` plus `readAt` and `onStale: 'reject'` to prevent lost updates.
96
+ `wait: 'confirmed'` means the server accepted the write. Schema model writes are optimistic by default; server rejection rolls back local state. To prevent lost updates, read with `snapshot(...)` to capture a `readAt`, then write with `onStale: 'reject'` the server rejects your update if someone else changed the row after that `readAt`.
86
97
 
87
98
  Claims coordinate writers; they do not block readers. Most users should stay on
88
- schema-backed reads/writes and `claim(...)`; do not teach manual protocol
89
- bookkeeping in the happy path.
99
+ schema-backed reads/writes and `claim(...)`; manual protocol bookkeeping is not
100
+ part of the happy path.
90
101
 
91
102
  All SDK errors extend `AbloError`. Important classes: `AbloClaimedError`, `AbloStaleContextError`, `AbloAuthenticationError`, `AbloPermissionError`, `AbloRateLimitError`, `AbloIdempotencyError`, `AbloConnectionError`, `AbloValidationError`, and `AbloServerError`.
92
103
 
93
104
  ## Schema Scope
94
105
 
95
- Teach schema as model fields and relations first. Advanced schema helpers such as `mutable`, `readOnly`, `field`, `indexed`, queries, and load strategies exist for offline/cache/indexing-heavy apps, but they should not be the first concept a new integration sees.
106
+ A schema is model fields and relations. Advanced schema helpers such as `mutable`, `readOnly`, `field`, `indexed`, queries, and load strategies exist for offline/cache/indexing-heavy apps; reach for them only after the basic field/relation schema is working.
96
107
 
97
108
  ## Storage Boundary
98
109
 
99
110
  Do not add `databaseURL` to `Ablo(...)`. Application and agent code use `ABLO_API_KEY`.
100
111
 
101
- Every schema model has a backing store. By default, Ablo stores rows for declared models, so `ablo.<model>.create/update/delete` write to Ablo-managed state. If the customer database is canonical, expose a Data Source endpoint and pass `apiKey: process.env.ABLO_API_KEY` to `dataSource({ schema, apiKey, load, list, commit, events })`. Customer-owned app database credentials stay private.
102
-
103
- Use `dataSource` from the root import:
112
+ Every schema model has a backing store. By default, Ablo stores rows for declared models, so `ablo.<model>.create/update/delete` write to Ablo-managed state. If the customer database is canonical, expose a Data Source endpoint. With Prisma or Drizzle this is ONE line — pass an ORM `adapter` and it owns the transaction, idempotency, and outbox (no hand-written `commit`/`events`):
104
113
 
105
114
  ```ts
106
- import { dataSource } from '@abloatai/ablo';
115
+ // app/api/ablo/source/route.ts
116
+ import { dataSourceNext } from '@abloatai/ablo/source/next';
117
+ import { prismaDataSource } from '@abloatai/ablo/source';
118
+ import { schema } from '@/ablo/schema';
119
+ import { prisma } from '@/lib/prisma';
120
+
121
+ export const { POST } = dataSourceNext({
122
+ schema,
123
+ apiKey: process.env.ABLO_API_KEY!,
124
+ adapter: prismaDataSource(prisma, schema), // or drizzleDataSource(db, tables)
125
+ });
107
126
  ```
108
127
 
128
+ `npx ablo init` generates this file for you (see CLI below). Customer-owned app database credentials stay private — Ablo only calls the endpoint.
129
+
109
130
  ## Sandboxes
110
131
 
111
132
  Public `/sandbox` is a deterministic visual demo. It should teach shared state,
@@ -132,7 +153,20 @@ Import from these public paths only:
132
153
  - `@abloatai/ablo/schema` — schema DSL.
133
154
  - `@abloatai/ablo/react` — React provider and hooks.
134
155
  - `@abloatai/ablo/testing` — test harnesses and mocks.
156
+ - `@abloatai/ablo/source` — `dataSource`, the `DataSourceAdapter` spine, `prismaDataSource`. For a customer-canonical Data Source endpoint.
157
+ - `@abloatai/ablo/source/next` — `dataSourceNext` (Next.js App Router `{ POST }`).
158
+ - `@abloatai/ablo/source/drizzle` — `drizzleDataSource`.
159
+ - `@abloatai/ablo/source/conformance` — `runDataSourceTests` to prove a custom adapter/handler.
160
+
161
+ Do not teach `/api`, `/agent`, `/ai-sdk`, `/core`, `/realtime`, or internal subpaths. (`/source` IS public — it's the Data Source endpoint surface above.)
162
+
163
+ ## CLI — agents run it NON-INTERACTIVELY
164
+
165
+ `ablo init` and other prompts need a TTY; an agent/CI run has none and will HANG. Always:
135
166
 
136
- Do not teach `/api`, `/agent`, `/ai-sdk`, `/core`, `/realtime`, `/source`, or internal subpaths.
167
+ - `npx ablo init --yes` (flags: `--framework`, `--auth`, `--storage`, `--no-agent`, `--no-pull`, `--no-install`, `--no-login`). Generates `ablo/schema.ts` + the `ablo/data-source.ts` endpoint above.
168
+ - Authenticate with the `ABLO_API_KEY` env var. Do NOT run `ablo login` (opens a browser).
169
+ - Adopt an existing DB: `npx ablo pull prisma [path]` / `npx ablo pull drizzle <module>`.
170
+ - `npx ablo dev --no-watch` (default watches forever); `npx ablo logs --no-follow` (default tails forever); `npx ablo mode test|live` (always pass the arg). `npx ablo push`/`status`/`pull`/`check`/`generate` are one-shot.
137
171
 
138
- Canonical docs to read before integrating: `quickstart`, `integration-guide`, `guarantees`, `client-behavior`, `data-sources`, `examples/existing-python-backend`, `api`, `examples/ai-sdk-tool`, and `examples/server-agent`.
172
+ Canonical docs to read before integrating: `quickstart`, `schema-contract`, `integration-guide`, `guarantees`, `client-behavior`, `data-sources`, `examples/existing-python-backend`, `api`, `examples/ai-sdk-tool`, and `examples/server-agent`.
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "State control API for AI agents and collaborative apps.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "engines": {
8
- "node": ">=22.0.0"
8
+ "node": ">=24.0.0"
9
9
  },
10
10
  "main": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts",
@@ -54,6 +54,46 @@
54
54
  "types": "./dist/source/index.d.ts",
55
55
  "import": "./dist/source/index.js",
56
56
  "default": "./dist/source/index.js"
57
+ },
58
+ "./source/conformance": {
59
+ "types": "./dist/source/conformance.d.ts",
60
+ "import": "./dist/source/conformance.js",
61
+ "default": "./dist/source/conformance.js"
62
+ },
63
+ "./source/drizzle": {
64
+ "types": "./dist/source/adapters/drizzle.d.ts",
65
+ "import": "./dist/source/adapters/drizzle.js",
66
+ "default": "./dist/source/adapters/drizzle.js"
67
+ },
68
+ "./source/next": {
69
+ "types": "./dist/source/next.d.ts",
70
+ "import": "./dist/source/next.js",
71
+ "default": "./dist/source/next.js"
72
+ },
73
+ "./keys": {
74
+ "types": "./dist/keys/index.d.ts",
75
+ "import": "./dist/keys/index.js",
76
+ "default": "./dist/keys/index.js"
77
+ },
78
+ "./wire": {
79
+ "types": "./dist/wire/index.d.ts",
80
+ "import": "./dist/wire/index.js",
81
+ "default": "./dist/wire/index.js"
82
+ },
83
+ "./server": {
84
+ "types": "./dist/server/index.d.ts",
85
+ "import": "./dist/server/index.js",
86
+ "default": "./dist/server/index.js"
87
+ },
88
+ "./server/next": {
89
+ "types": "./dist/server/next.d.ts",
90
+ "import": "./dist/server/next.js",
91
+ "default": "./dist/server/next.js"
92
+ },
93
+ "./webhooks": {
94
+ "types": "./dist/webhooks/index.d.ts",
95
+ "import": "./dist/webhooks/index.js",
96
+ "default": "./dist/webhooks/index.js"
57
97
  }
58
98
  },
59
99
  "files": [
@@ -118,11 +158,15 @@
118
158
  "url": "https://github.com/Abloatai/ablo/issues"
119
159
  },
120
160
  "peerDependencies": {
121
- "react": "^19.0.0"
161
+ "react": "^19.0.0",
162
+ "drizzle-orm": ">=0.30.0"
122
163
  },
123
164
  "peerDependenciesMeta": {
124
165
  "react": {
125
166
  "optional": true
167
+ },
168
+ "drizzle-orm": {
169
+ "optional": true
126
170
  }
127
171
  },
128
172
  "dependencies": {
@@ -140,6 +184,7 @@
140
184
  "@testing-library/react": "^16.0.0",
141
185
  "@testing-library/jest-dom": "^6.6.0",
142
186
  "ai": "^6.0.0",
187
+ "drizzle-orm": "^0.45.2",
143
188
  "fake-indexeddb": "^6.0.0",
144
189
  "fast-check": "^3.0.0",
145
190
  "jest": "^29.7.0",