@abloatai/ablo 0.8.0 → 0.9.1

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 (165) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/README.md +33 -28
  3. package/dist/BaseSyncedStore.d.ts +83 -0
  4. package/dist/BaseSyncedStore.js +194 -2
  5. package/dist/Model.d.ts +42 -0
  6. package/dist/Model.js +103 -44
  7. package/dist/agent/session.js +3 -3
  8. package/dist/ai-sdk/coordination-context.js +4 -0
  9. package/dist/ai-sdk/index.d.ts +56 -47
  10. package/dist/ai-sdk/index.js +56 -47
  11. package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
  12. package/dist/ai-sdk/intent-broadcast.js +11 -4
  13. package/dist/ai-sdk/wrap.d.ts +14 -11
  14. package/dist/ai-sdk/wrap.js +11 -13
  15. package/dist/auth/credentialSource.d.ts +34 -0
  16. package/dist/auth/credentialSource.js +63 -0
  17. package/dist/auth/index.d.ts +2 -22
  18. package/dist/auth/index.js +4 -42
  19. package/dist/auth/schemas.d.ts +35 -0
  20. package/dist/auth/schemas.js +53 -0
  21. package/dist/client/Ablo.d.ts +160 -42
  22. package/dist/client/Ablo.js +145 -75
  23. package/dist/client/ApiClient.d.ts +20 -4
  24. package/dist/client/ApiClient.js +166 -28
  25. package/dist/client/auth.d.ts +14 -5
  26. package/dist/client/auth.js +60 -7
  27. package/dist/client/createInternalComponents.d.ts +2 -0
  28. package/dist/client/createInternalComponents.js +8 -1
  29. package/dist/client/createModelProxy.d.ts +130 -66
  30. package/dist/client/createModelProxy.js +152 -49
  31. package/dist/client/httpClient.d.ts +71 -0
  32. package/dist/client/httpClient.js +69 -0
  33. package/dist/client/identity.d.ts +2 -6
  34. package/dist/client/identity.js +49 -11
  35. package/dist/client/index.d.ts +1 -0
  36. package/dist/client/index.js +1 -0
  37. package/dist/client/registerDataSource.d.ts +3 -3
  38. package/dist/client/registerDataSource.js +11 -9
  39. package/dist/client/validateAbloOptions.js +1 -1
  40. package/dist/core/DatabaseManager.js +30 -2
  41. package/dist/core/openIDBWithTimeout.d.ts +36 -0
  42. package/dist/core/openIDBWithTimeout.js +88 -1
  43. package/dist/errorCodes.d.ts +70 -1
  44. package/dist/errorCodes.js +108 -9
  45. package/dist/errors.d.ts +2 -2
  46. package/dist/errors.js +72 -22
  47. package/dist/index.d.ts +17 -8
  48. package/dist/index.js +15 -6
  49. package/dist/keys/index.d.ts +16 -1
  50. package/dist/keys/index.js +26 -6
  51. package/dist/mutators/UndoManager.d.ts +158 -50
  52. package/dist/mutators/UndoManager.js +345 -22
  53. package/dist/mutators/inverseOp.d.ts +129 -0
  54. package/dist/mutators/inverseOp.js +74 -0
  55. package/dist/mutators/readerActions.d.ts +1 -1
  56. package/dist/mutators/undoApply.d.ts +42 -0
  57. package/dist/mutators/undoApply.js +143 -0
  58. package/dist/query/client.d.ts +10 -9
  59. package/dist/query/client.js +3 -6
  60. package/dist/react/AbloProvider.d.ts +23 -126
  61. package/dist/react/AbloProvider.js +62 -199
  62. package/dist/react/context.d.ts +31 -0
  63. package/dist/react/useAblo.d.ts +2 -2
  64. package/dist/react/useCurrentUserId.d.ts +1 -1
  65. package/dist/react/useCurrentUserId.js +1 -1
  66. package/dist/react/useMutators.js +19 -12
  67. package/dist/schema/ddl.d.ts +34 -3
  68. package/dist/schema/ddl.js +162 -4
  69. package/dist/schema/index.d.ts +5 -1
  70. package/dist/schema/index.js +13 -1
  71. package/dist/schema/model.d.ts +11 -0
  72. package/dist/schema/model.js +2 -0
  73. package/dist/schema/openapi.d.ts +28 -0
  74. package/dist/schema/openapi.js +118 -0
  75. package/dist/schema/plane.d.ts +23 -0
  76. package/dist/schema/plane.js +19 -0
  77. package/dist/schema/relation.d.ts +20 -0
  78. package/dist/schema/serialize.d.ts +4 -0
  79. package/dist/schema/serialize.js +4 -0
  80. package/dist/schema/sync-delta-row.d.ts +157 -0
  81. package/dist/schema/sync-delta-row.js +102 -0
  82. package/dist/schema/sync-delta-wire.d.ts +180 -0
  83. package/dist/schema/sync-delta-wire.js +102 -0
  84. package/dist/server/adapter.d.ts +156 -0
  85. package/dist/server/adapter.js +19 -0
  86. package/dist/server/commit.d.ts +82 -0
  87. package/dist/server/commit.js +1 -0
  88. package/dist/server/index.d.ts +14 -0
  89. package/dist/server/index.js +1 -0
  90. package/dist/server/next.d.ts +51 -0
  91. package/dist/server/next.js +47 -0
  92. package/dist/server/read-config.d.ts +60 -0
  93. package/dist/server/read-config.js +8 -0
  94. package/dist/server/storage-mode.d.ts +17 -0
  95. package/dist/server/storage-mode.js +12 -0
  96. package/dist/source/adapter.d.ts +65 -0
  97. package/dist/source/adapter.js +20 -0
  98. package/dist/source/adapters/drizzle.d.ts +43 -0
  99. package/dist/source/adapters/drizzle.js +185 -0
  100. package/dist/source/adapters/memory.d.ts +12 -0
  101. package/dist/source/adapters/memory.js +114 -0
  102. package/dist/source/adapters/prisma.d.ts +57 -0
  103. package/dist/source/adapters/prisma.js +176 -0
  104. package/dist/source/conformance.d.ts +32 -0
  105. package/dist/source/conformance.js +134 -0
  106. package/dist/source/contract.d.ts +144 -0
  107. package/dist/source/contract.js +99 -0
  108. package/dist/source/index.d.ts +62 -10
  109. package/dist/source/index.js +99 -0
  110. package/dist/source/migrations.d.ts +14 -0
  111. package/dist/source/migrations.js +39 -0
  112. package/dist/source/next.d.ts +33 -0
  113. package/dist/source/next.js +26 -0
  114. package/dist/sync/BootstrapHelper.d.ts +10 -0
  115. package/dist/sync/BootstrapHelper.js +10 -15
  116. package/dist/sync/ConnectionManager.d.ts +55 -1
  117. package/dist/sync/ConnectionManager.js +155 -16
  118. package/dist/sync/HydrationCoordinator.d.ts +93 -17
  119. package/dist/sync/HydrationCoordinator.js +238 -39
  120. package/dist/sync/NetworkProbe.d.ts +58 -24
  121. package/dist/sync/NetworkProbe.js +118 -42
  122. package/dist/sync/SyncWebSocket.d.ts +45 -70
  123. package/dist/sync/SyncWebSocket.js +70 -36
  124. package/dist/sync/createIntentStream.js +10 -1
  125. package/dist/types/streams.d.ts +9 -0
  126. package/dist/utils/mobx-setup.js +1 -0
  127. package/dist/webhooks/events.d.ts +38 -0
  128. package/dist/webhooks/events.js +40 -0
  129. package/dist/webhooks/index.d.ts +10 -0
  130. package/dist/webhooks/index.js +10 -0
  131. package/dist/wire/errorEnvelope.d.ts +34 -0
  132. package/dist/wire/errorEnvelope.js +86 -0
  133. package/dist/wire/frames.d.ts +119 -0
  134. package/dist/wire/frames.js +1 -0
  135. package/dist/wire/index.d.ts +24 -0
  136. package/dist/wire/index.js +21 -0
  137. package/dist/wire/listEnvelope.d.ts +45 -0
  138. package/dist/wire/listEnvelope.js +17 -0
  139. package/docs/api.md +47 -44
  140. package/docs/cli.md +44 -44
  141. package/docs/client-behavior.md +30 -30
  142. package/docs/coordination.md +33 -36
  143. package/docs/data-sources.md +35 -15
  144. package/docs/examples/agent-human.md +45 -43
  145. package/docs/examples/ai-sdk-tool.md +20 -16
  146. package/docs/examples/existing-python-backend.md +16 -12
  147. package/docs/examples/nextjs.md +14 -12
  148. package/docs/examples/scoped-agent.md +1 -1
  149. package/docs/examples/server-agent.md +24 -21
  150. package/docs/guarantees.md +15 -13
  151. package/docs/index.md +2 -2
  152. package/docs/integration-guide.md +30 -30
  153. package/docs/interaction-model.md +19 -23
  154. package/docs/mcp/claude-code.md +3 -3
  155. package/docs/mcp/cursor.md +1 -1
  156. package/docs/mcp/windsurf.md +2 -2
  157. package/docs/mcp.md +6 -6
  158. package/docs/quickstart.md +41 -31
  159. package/docs/react.md +13 -9
  160. package/docs/schema-contract.md +12 -10
  161. package/docs/the-loop.md +21 -0
  162. package/examples/data-source/README.md +4 -5
  163. package/examples/data-source/customer-server.ts +27 -25
  164. package/llms.txt +28 -5
  165. package/package.json +43 -3
@@ -3,7 +3,7 @@
3
3
  ## Install
4
4
 
5
5
  ```bash
6
- claude mcp add --transport http ablo-sync https://<your-app>/api/mcp
6
+ claude mcp add --transport http ablo https://<your-app>/api/mcp
7
7
  ```
8
8
 
9
9
  That's it — no token or header needed. The endpoint is public and serves
@@ -18,14 +18,14 @@ In Claude Code, run:
18
18
  /mcp list
19
19
  ```
20
20
 
21
- You should see `ablo-sync` with the integration tools enumerated:
21
+ You should see `ablo` with the integration tools enumerated:
22
22
  `search_ablo_docs`, `get_recipe`, `get_api_surface`, `validate_schema`,
23
23
  `scaffold_app`.
24
24
 
25
25
  ## Removing
26
26
 
27
27
  ```bash
28
- claude mcp remove ablo-sync
28
+ claude mcp remove ablo
29
29
  ```
30
30
 
31
31
  ## More
@@ -7,7 +7,7 @@ Add the Ablo Sync MCP server to Cursor's `mcp.json`:
7
7
  ```json
8
8
  {
9
9
  "mcpServers": {
10
- "ablo-sync": {
10
+ "ablo": {
11
11
  "transport": "http",
12
12
  "url": "https://<your-app>/api/mcp"
13
13
  }
@@ -7,7 +7,7 @@ Add the Ablo Sync MCP server to Windsurf's MCP config:
7
7
  ```json
8
8
  {
9
9
  "mcpServers": {
10
- "ablo-sync": {
10
+ "ablo": {
11
11
  "transport": "http",
12
12
  "url": "https://<your-app>/api/mcp"
13
13
  }
@@ -22,7 +22,7 @@ the endpoint is public and serves only docs, schema lint, and scaffolds.
22
22
  ## Verify
23
23
 
24
24
  Cascade's MCP panel lists every configured server with its tools. You
25
- should see `ablo-sync` with the integration tools enumerated:
25
+ should see `ablo` with the integration tools enumerated:
26
26
  `search_ablo_docs`, `get_recipe`, `get_api_surface`, `validate_schema`,
27
27
  `scaffold_app`.
28
28
 
package/docs/mcp.md CHANGED
@@ -33,10 +33,10 @@ Each tool mirrors an SDK verb, scoped to a model + id:
33
33
  |---|---|---|
34
34
  | `get_model` | `ablo.<model>.get(id)` | read latest state + active claims |
35
35
  | `list_models` | `ablo.<model>.list({…})` | cursor-paginated list with filters |
36
- | `create_model` | `ablo.<model>.create(data)` | guarded create |
37
- | `update_model` | `ablo.<model>.update(id, …)` | guarded update |
38
- | `delete_model` | `ablo.<model>.delete(id)` | guarded delete |
39
- | `claim_model` | `ablo.<model>.claim(id)` | acquire / queue a coordination lease |
36
+ | `create_model` | `ablo.<model>.create({ data })` | guarded create |
37
+ | `update_model` | `ablo.<model>.update({ id, … })` | guarded update |
38
+ | `delete_model` | `ablo.<model>.delete({ id })` | guarded delete |
39
+ | `claim_model` | `ablo.<model>.claim({ id })` | acquire / queue a coordination lease |
40
40
  | `release_claim` | — | release the lease so others proceed |
41
41
 
42
42
  The agent-facing contract — the safe loop, the "derive idempotency keys from
@@ -64,7 +64,7 @@ server above, never here.
64
64
  Point your assistant at the hosted endpoint — no auth, no token:
65
65
 
66
66
  ```bash
67
- claude mcp add --transport http ablo-sync https://<your-app>/api/mcp
67
+ claude mcp add --transport http ablo https://<your-app>/api/mcp
68
68
  ```
69
69
 
70
70
  Per-client walkthroughs:
@@ -87,7 +87,7 @@ Per-client walkthroughs:
87
87
 
88
88
  #### Resources
89
89
 
90
- Every doc file is addressable at `ablo-sync-engine://docs/{name}`, so a
90
+ Every doc file is addressable at `ablo://docs/{name}`, so a
91
91
  client can list the corpus and fetch individual files on demand instead of
92
92
  loading everything into context.
93
93
 
@@ -41,13 +41,18 @@ export const ablo = Ablo({
41
41
  await ablo.ready();
42
42
 
43
43
  const created = await ablo.weatherReports.create({
44
- location: 'Stockholm',
45
- status: 'pending',
44
+ data: {
45
+ location: 'Stockholm',
46
+ status: 'pending',
47
+ },
46
48
  });
47
49
 
48
- const updated = await ablo.weatherReports.update(created.id, {
49
- status: 'ready',
50
- forecast: 'Light rain, 13C',
50
+ const updated = await ablo.weatherReports.update({
51
+ id: created.id,
52
+ data: {
53
+ status: 'ready',
54
+ forecast: 'Light rain, 13C',
55
+ },
51
56
  });
52
57
 
53
58
  console.log({ id: updated.id, status: updated.status });
@@ -69,40 +74,45 @@ ABLO_API_KEY=sk_test_... npx tsx examples/quickstart.ts
69
74
  ## Add coordination for slow work
70
75
 
71
76
  When AI or background work will touch an existing row for more than a quick
72
- write, coordinate through `claim(id, work)`. It claims the row and hands the row
73
- back; `claim.state(id)` reads who is currently working on it without blocking;
74
- and you write the usual way with `ablo.<model>.update(id, ...)`.
77
+ write, coordinate through `claim({ id })`. It claims the row and hands a handle
78
+ back; `claim.state({ id })` reads who is currently working on it without blocking;
79
+ and you write the usual way with `ablo.<model>.update({ id, data })`.
75
80
 
76
81
  Claims don't lock. If another writer holds the row, `claim` waits for them,
77
82
  re-reads the fresh row, then hands it to you — so two writers serialize instead
78
83
  of clobbering. Normal reads still work while the claim is held. If a server read
79
84
  should not return a row while someone else is mid-edit, pass `ifClaimed: 'wait'`
80
- to wait for the claim to clear, or `ifClaimed: 'fail'` to error out instead. The
81
- callback form releases the claim when the callback returns or throws.
85
+ to wait for the claim to clear, or `ifClaimed: 'fail'` to error out instead.
86
+ Call `handle.release()` when your work is done.
82
87
 
83
88
  ```ts
84
89
  // Claim the row so other participants serialize behind us while we work.
85
- await ablo.weatherReports.claim(
86
- 'weather_stockholm',
87
- async (report) => {
88
- // Your existing weather tool or agent call. While this runs, other clients
89
- // see that weather_stockholm is being checked.
90
- const weather = await weatherAgent.getWeather(report.location);
91
-
92
- await ablo.weatherReports.update(report.id, {
93
- status: 'ready',
94
- forecast: weather.summary,
95
- });
90
+ const handle = await ablo.weatherReports.claim({
91
+ id: 'weather_stockholm',
92
+ action: 'checking_weather',
93
+ ttl: '2m',
94
+ });
95
+
96
+ // Your existing weather tool or agent call. While this runs, other clients
97
+ // see that weather_stockholm is being checked.
98
+ const weather = await weatherAgent.getWeather(handle.data.location);
99
+
100
+ await ablo.weatherReports.update({
101
+ id: handle.data.id,
102
+ data: {
103
+ status: 'ready',
104
+ forecast: weather.summary,
96
105
  },
97
- { action: 'checking_weather', ttl: '2m' },
98
- );
106
+ });
107
+
108
+ await handle.release();
99
109
  ```
100
110
 
101
111
  Ablo does not fetch the weather. If another participant already holds the row,
102
112
  `claim` waits for them to finish, re-reads, and then hands you the fresh row.
103
- While you hold the claim, `update(id, ...)` rejects with `AbloStaleContextError`
113
+ While you hold the claim, `update({ id, data })` rejects with `AbloStaleContextError`
104
114
  if someone else changed the row first — so you never overwrite work you didn't
105
- see. The claim releases automatically once the callback returns or throws.
115
+ see. Call `handle.release()` once your work is done.
106
116
 
107
117
  ## Multiplayer and claimed work
108
118
 
@@ -110,19 +120,19 @@ There is no separate multiplayer mode. Use the same schema client for human UI,
110
120
  server actions, and agents; Ablo fans out confirmed writes and keeps active
111
121
  claims visible on the same model row.
112
122
 
113
- `claim.state(id)` tells you when another human or agent is active on the same row.
114
- For schema clients, `claim(id, work)` waits fairly, re-reads, and then lets you
123
+ `claim.state({ id })` tells you when another human or agent is active on the same row.
124
+ For schema clients, `claim({ id })` waits fairly, re-reads, and then lets you
115
125
  write through the model.
116
126
 
117
127
  ```ts
118
- const active = ablo.weatherReports.claim.state('weather_stockholm');
128
+ const active = ablo.weatherReports.claim.state({ id: 'weather_stockholm' });
119
129
  if (active) {
120
130
  console.log(`${active.heldBy} is ${active.action}`);
121
131
  }
122
132
 
123
- await ablo.weatherReports.claim('weather_stockholm', async (report) => {
124
- await ablo.weatherReports.update(report.id, { status: 'ready' });
125
- });
133
+ const handle = await ablo.weatherReports.claim({ id: 'weather_stockholm' });
134
+ await ablo.weatherReports.update({ id: handle.data.id, data: { status: 'ready' } });
135
+ await handle.release();
126
136
  ```
127
137
 
128
138
  Use `{ wait: false }` on `claim` when work should be skipped instead of queued
package/docs/react.md CHANGED
@@ -75,7 +75,7 @@ import { useAblo } from '@abloatai/ablo/react';
75
75
 
76
76
  export function ReportView({ report: serverReport }: { report: { id: string; location: string } }) {
77
77
  const report = useAblo((ablo) => ablo.weatherReports.get(serverReport.id)) ?? serverReport;
78
- const active = useAblo((ablo) => ablo.weatherReports.claim.state(serverReport.id));
78
+ const active = useAblo((ablo) => ablo.weatherReports.claim.state({ id: serverReport.id }));
79
79
  const claimed = Boolean(active);
80
80
 
81
81
  return <article>{report.location}</article>;
@@ -90,7 +90,7 @@ The hook:
90
90
  deltas arrive.
91
91
  3. Lets Server Component data stay outside the hook: use `?? serverReport` when a
92
92
  parent already loaded the row.
93
- 4. Works for coordination state too, such as `ablo.weatherReports.claim.state(id)`.
93
+ 4. Works for coordination state too, such as `ablo.weatherReports.claim.state({ id })`.
94
94
 
95
95
  Use the zero-argument form only when you need the full client for callbacks,
96
96
  effects, or writes:
@@ -117,12 +117,12 @@ const reports = useAblo((ablo) =>
117
117
  ## Server Load
118
118
 
119
119
  ```tsx
120
- const report = await ablo.weatherReports.retrieve(id);
120
+ const report = await ablo.weatherReports.retrieve({ id });
121
121
  ```
122
122
 
123
123
  Use `retrieve` in Server Components when the row may not be in the local pool
124
124
  yet — it hydrates from the local store and the server, and returns a Promise, so
125
- `await` it. (Server reads come in two shapes: `retrieve(id)` for one row and
125
+ `await` it. (Server reads come in two shapes: `retrieve({ id })` for one row and
126
126
  `list({ where })` for many; both are async. The synchronous local reads are
127
127
  `get`/`getAll`/`getCount`, used in render below.)
128
128
 
@@ -134,7 +134,9 @@ For Server Actions and route handlers, call the SDK directly:
134
134
  import { ablo } from '@/lib/ablo';
135
135
 
136
136
  const snap = ablo.snapshot({ weatherReports: id });
137
- await ablo.weatherReports.update(id, patch, {
137
+ await ablo.weatherReports.update({
138
+ id,
139
+ data: patch,
138
140
  readAt: snap.stamp,
139
141
  onStale: 'reject',
140
142
  wait: 'confirmed',
@@ -150,11 +152,13 @@ const ablo = useAblo();
150
152
  async function markReady() {
151
153
  if (!ablo) return;
152
154
  const snap = ablo.snapshot({ weatherReports: id });
153
- await ablo.weatherReports.update(
155
+ await ablo.weatherReports.update({
154
156
  id,
155
- { status: 'ready' },
156
- { readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
157
- );
157
+ data: { status: 'ready' },
158
+ readAt: snap.stamp,
159
+ onStale: 'reject',
160
+ wait: 'confirmed',
161
+ });
158
162
  }
159
163
  ```
160
164
 
@@ -37,8 +37,10 @@ export const ablo = Ablo({
37
37
  await ablo.ready();
38
38
 
39
39
  const report = await ablo.weatherReports.create({
40
- location: 'Stockholm',
41
- status: 'pending',
40
+ data: {
41
+ location: 'Stockholm',
42
+ status: 'pending',
43
+ },
42
44
  });
43
45
  ```
44
46
 
@@ -52,7 +54,7 @@ data.
52
54
  Use async reads when the row may not be local:
53
55
 
54
56
  ```ts
55
- const report = await ablo.weatherReports.retrieve(reportId);
57
+ const report = await ablo.weatherReports.retrieve({ id: reportId });
56
58
  const ready = await ablo.weatherReports.list({ where: { status: 'ready' } });
57
59
  ```
58
60
 
@@ -66,7 +68,7 @@ const pending = ablo.weatherReports.getAll({ where: { status: 'pending' } });
66
68
  Use model writes for every actor:
67
69
 
68
70
  ```ts
69
- await ablo.weatherReports.update(reportId, { status: 'ready' }, { wait: 'confirmed' });
71
+ await ablo.weatherReports.update({ id: reportId, data: { status: 'ready' }, wait: 'confirmed' });
70
72
  ```
71
73
 
72
74
  ## Coordination
@@ -75,14 +77,14 @@ Agents and background jobs often read, call a tool or model, then write later.
75
77
  Wrap that slow span in `claim`:
76
78
 
77
79
  ```ts
78
- await ablo.weatherReports.claim(reportId, async (report) => {
79
- const forecast = await getForecast(report.location);
80
- await ablo.weatherReports.update(report.id, { status: 'ready', forecast });
81
- });
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();
82
84
  ```
83
85
 
84
- If another writer already holds the row, `claim` waits, re-reads, and gives your
85
- callback the fresh row. Reads stay open; only acting on the row serializes.
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.
86
88
 
87
89
  ## Storage boundary
88
90
 
@@ -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.
@@ -38,9 +38,9 @@ signed bytes — flip the API key and you'll see a 401.
38
38
  3. **The customer DB stays canonical.** Ablo never sees rows
39
39
  directly; it only sees the response payload from the customer's
40
40
  handler.
41
- 4. **The outbox feed.** Writes that bypass Ablo (cron, dashboards,
42
- batch imports) show up on the next `events` poll so Ablo can fan
43
- 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.
44
44
 
45
45
  ## Production wiring
46
46
 
@@ -96,8 +96,7 @@ data layer. The handler shape stays the same:
96
96
  - `tasks.load({ id })` -> `db.task.findUnique({ where: { id } })`
97
97
  - `tasks.list({ query })` -> `db.task.findMany({ take, cursor })`
98
98
  - `tasks.commit({ operations, clientTxId })` -> `db.$transaction` that
99
- applies each `op` and tags the row with `clientTxId` for idempotent
100
- retries
99
+ applies each `op` and writes an outbox marker with `clientTxId` before commit
101
100
  - `events({ cursor, limit })` -> read from your outbox table, return
102
101
  rows with their `clientTxId` (Ablo dedupes its own commits) and the
103
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
package/llms.txt CHANGED
@@ -109,14 +109,24 @@ A schema is model fields and relations. Advanced schema helpers such as `mutable
109
109
 
110
110
  Do not add `databaseURL` to `Ablo(...)`. Application and agent code use `ABLO_API_KEY`.
111
111
 
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 and pass `apiKey: process.env.ABLO_API_KEY` to `dataSource({ schema, apiKey, load, list, commit, events })`. Customer-owned app database credentials stay private.
113
-
114
- 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`):
115
113
 
116
114
  ```ts
117
- 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
+ });
118
126
  ```
119
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
+
120
130
  ## Sandboxes
121
131
 
122
132
  Public `/sandbox` is a deterministic visual demo. It should teach shared state,
@@ -143,7 +153,20 @@ Import from these public paths only:
143
153
  - `@abloatai/ablo/schema` — schema DSL.
144
154
  - `@abloatai/ablo/react` — React provider and hooks.
145
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:
146
166
 
147
- 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.
148
171
 
149
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.8.0",
3
+ "version": "0.9.1",
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",
@@ -55,10 +55,45 @@
55
55
  "import": "./dist/source/index.js",
56
56
  "default": "./dist/source/index.js"
57
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
+ },
58
73
  "./keys": {
59
74
  "types": "./dist/keys/index.d.ts",
60
75
  "import": "./dist/keys/index.js",
61
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"
62
97
  }
63
98
  },
64
99
  "files": [
@@ -123,11 +158,15 @@
123
158
  "url": "https://github.com/Abloatai/ablo/issues"
124
159
  },
125
160
  "peerDependencies": {
126
- "react": "^19.0.0"
161
+ "react": "^19.0.0",
162
+ "drizzle-orm": ">=0.30.0"
127
163
  },
128
164
  "peerDependenciesMeta": {
129
165
  "react": {
130
166
  "optional": true
167
+ },
168
+ "drizzle-orm": {
169
+ "optional": true
131
170
  }
132
171
  },
133
172
  "dependencies": {
@@ -145,6 +184,7 @@
145
184
  "@testing-library/react": "^16.0.0",
146
185
  "@testing-library/jest-dom": "^6.6.0",
147
186
  "ai": "^6.0.0",
187
+ "drizzle-orm": "^0.45.2",
148
188
  "fake-indexeddb": "^6.0.0",
149
189
  "fast-check": "^3.0.0",
150
190
  "jest": "^29.7.0",