@abloatai/ablo 0.9.1 → 0.9.3

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 (79) hide show
  1. package/AGENTS.md +84 -0
  2. package/CHANGELOG.md +40 -0
  3. package/README.md +53 -27
  4. package/dist/BaseSyncedStore.d.ts +2 -36
  5. package/dist/BaseSyncedStore.js +11 -55
  6. package/dist/NetworkMonitor.js +4 -1
  7. package/dist/SyncClient.d.ts +22 -5
  8. package/dist/SyncClient.js +77 -0
  9. package/dist/SyncEngineContext.js +5 -1
  10. package/dist/agent/index.js +1 -1
  11. package/dist/api/index.d.ts +1 -1
  12. package/dist/auth/index.js +3 -1
  13. package/dist/cli.cjs +302645 -0
  14. package/dist/client/Ablo.d.ts +19 -52
  15. package/dist/client/Ablo.js +30 -106
  16. package/dist/client/ApiClient.d.ts +1 -113
  17. package/dist/client/ApiClient.js +39 -238
  18. package/dist/client/auth.js +32 -2
  19. package/dist/client/createInternalComponents.js +1 -1
  20. package/dist/client/createModelProxy.d.ts +9 -0
  21. package/dist/client/createModelProxy.js +34 -10
  22. package/dist/client/httpClient.d.ts +5 -6
  23. package/dist/client/httpClient.js +2 -3
  24. package/dist/client/index.d.ts +1 -1
  25. package/dist/client/persistence.d.ts +6 -1
  26. package/dist/client/persistence.js +1 -1
  27. package/dist/client/registerDataSource.d.ts +4 -4
  28. package/dist/client/registerDataSource.js +39 -31
  29. package/dist/client/writeOptionsSchema.d.ts +50 -0
  30. package/dist/client/writeOptionsSchema.js +57 -0
  31. package/dist/core/index.d.ts +18 -26
  32. package/dist/core/index.js +22 -46
  33. package/dist/errorCodes.d.ts +13 -0
  34. package/dist/errorCodes.js +19 -4
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.js +8 -1
  37. package/dist/interfaces/index.d.ts +14 -4
  38. package/dist/mutators/UndoManager.d.ts +48 -5
  39. package/dist/mutators/UndoManager.js +166 -1
  40. package/dist/react/AbloProvider.d.ts +18 -8
  41. package/dist/react/index.d.ts +1 -1
  42. package/dist/react/index.js +1 -1
  43. package/dist/react/useUndoScope.js +7 -0
  44. package/dist/schema/ddl.js +2 -1
  45. package/dist/schema/field.js +2 -1
  46. package/dist/schema/serialize.js +2 -1
  47. package/dist/server/commit.d.ts +4 -5
  48. package/dist/server/storage-mode.d.ts +7 -0
  49. package/dist/server/storage-mode.js +6 -0
  50. package/dist/source/adapters/drizzle.js +3 -2
  51. package/dist/source/adapters/kysely.d.ts +68 -0
  52. package/dist/source/adapters/kysely.js +210 -0
  53. package/dist/source/adapters/memory.js +2 -1
  54. package/dist/source/adapters/prisma.js +3 -2
  55. package/dist/source/index.js +2 -1
  56. package/dist/transactions/TransactionQueue.d.ts +6 -7
  57. package/dist/transactions/TransactionQueue.js +33 -9
  58. package/dist/types/streams.d.ts +2 -1
  59. package/dist/utils/duration.js +3 -2
  60. package/dist/wire/frames.d.ts +6 -8
  61. package/docs/api.md +1 -1
  62. package/docs/cli.md +17 -4
  63. package/docs/client-behavior.md +1 -1
  64. package/docs/data-sources.md +129 -125
  65. package/docs/examples/ai-sdk-tool.md +11 -5
  66. package/docs/examples/existing-python-backend.md +26 -4
  67. package/docs/examples/nextjs.md +3 -2
  68. package/docs/examples/scoped-agent.md +38 -11
  69. package/docs/guarantees.md +2 -2
  70. package/docs/identity.md +86 -59
  71. package/docs/index.md +2 -2
  72. package/docs/integration-guide.md +89 -61
  73. package/docs/mcp.md +1 -1
  74. package/docs/quickstart.md +84 -37
  75. package/docs/react.md +39 -28
  76. package/docs/schema-contract.md +2 -4
  77. package/llms-full.txt +360 -0
  78. package/llms.txt +30 -18
  79. package/package.json +23 -3
@@ -1,74 +1,121 @@
1
1
  # Quickstart
2
2
 
3
- Start building with Ablo in two steps: install, then declare one model and write
4
- through its generated client.
3
+ Build with Ablo on **your own database**. You declare a small Ablo schema for the
4
+ models humans and agents edit together, hand the client your Postgres
5
+ `DATABASE_URL`, and coordinate every write through `ablo.<model>`. Your database
6
+ is the system of record — Ablo never hosts your data. It is the transaction
7
+ layer on top: it registers your connection, commits every write there behind
8
+ row-level security, and fans the confirmed rows out to every connected client.
5
9
 
6
- If you already have a backend and database, still start here. The SDK call shape
7
- is the same; [Integration Guide](./integration-guide.md) explains when to use
8
- Ablo-managed state versus a Data Source that calls your existing API service.
9
-
10
- ## 1. Install and set a sandbox key
10
+ ## 1. Install and get a key
11
11
 
12
12
  ```bash
13
13
  npm install @abloatai/ablo
14
+ npx ablo login
15
+ ```
16
+
17
+ `ablo login` opens the browser — sign in (or sign up) and a `sk_test_` key is
18
+ saved locally for the CLI. Later, `npx ablo dev` (step 4) writes
19
+ `ABLO_API_KEY` into your `.env.local` so the SDK finds it too — no manual
20
+ copy-paste. In CI, or to manage it by hand, set it yourself instead:
21
+
22
+ ```bash
14
23
  export ABLO_API_KEY=sk_test_...
15
24
  ```
16
25
 
17
- `ABLO_API_KEY` is for trusted server runtimes. Browser apps should use the React
18
- provider with a scoped session route, not a bundled API key.
26
+ Every SDK and CLI call needs a key. Test and live keys work like Stripe's —
27
+ except both point at databases *you* own: `sk_test_*` for your dev database,
28
+ `sk_live_*` for production. There is no keyless mode; the public `/sandbox` page
29
+ is a hosted demo, not your app.
19
30
 
20
- ## 2. Declare schema and write state
31
+ ## 2. Declare your Ablo schema
21
32
 
22
- Your schema is the contract. It generates `ablo.<model>` methods for app code,
23
- server actions, agents, React reads, and Data Source requests.
33
+ The schema is the contract it generates `ablo.<model>` methods for app code,
34
+ server actions, agents, and React reads. Declare **only the synced models** Ablo
35
+ coordinates; your auth, billing, and other tables stay in your own Drizzle schema,
36
+ owned by your own migrations.
24
37
 
25
38
  ```ts
26
- import Ablo from '@abloatai/ablo';
39
+ // ablo/schema.ts
27
40
  import { defineSchema, model, z } from '@abloatai/ablo/schema';
28
41
 
29
- const schema = defineSchema({
42
+ export const schema = defineSchema({
30
43
  weatherReports: model({
31
44
  location: z.string(),
32
45
  status: z.enum(['pending', 'ready']),
33
46
  forecast: z.string().optional(),
34
47
  }),
35
48
  });
49
+ ```
50
+
51
+ ## 3. Point Ablo at your database
52
+
53
+ The client takes your schema, your key, and your `DATABASE_URL`. On first
54
+ connect Ablo registers the connection (sent once over TLS, stored sealed, never
55
+ echoed back) and from then on commits every write directly to your Postgres.
56
+
57
+ ```bash
58
+ # .env — server runtime only, never the browser
59
+ DATABASE_URL=postgres://ablo_app:...@host:5432/db
60
+ ABLO_API_KEY=sk_test_...
61
+ ```
62
+
63
+ ```ts
64
+ // ablo/client.ts
65
+ import Ablo from '@abloatai/ablo';
66
+ import { schema } from './schema';
36
67
 
37
68
  export const ablo = Ablo({
38
69
  schema,
39
70
  apiKey: process.env.ABLO_API_KEY,
71
+ databaseUrl: process.env.DATABASE_URL, // your Postgres — rows live here, never with Ablo
40
72
  });
73
+ ```
74
+
75
+ Use a dedicated **non-superuser role** for the connection — Ablo enforces
76
+ tenant isolation with row-level security, so the server rejects superuser or
77
+ `BYPASSRLS` roles outright.
78
+
79
+ Don't want a connection string to leave your infrastructure? Keep
80
+ `DATABASE_URL` in your app only and expose one signed **Data Source endpoint**
81
+ built from an ORM adapter instead — same product, same writes, see
82
+ [Connect Your Database](./data-sources.md). In that setup, omit `databaseUrl`
83
+ from `Ablo(...)`.
84
+
85
+ ## 4. Provision your tables, then push the schema
86
+
87
+ ```bash
88
+ npx ablo migrate # creates your synced-model tables (with row-level security)
89
+ # in YOUR database — your other tables are left untouched
90
+ npx ablo dev # pushes the schema (test mode), writes ABLO_API_KEY to
91
+ # .env.local, and re-pushes on every save — the dev loop
92
+ ```
93
+
94
+ `ablo dev` (or one-shot `npx ablo push`) uploads the schema *definition* —
95
+ model names, fields, types. That metadata is the only thing Ablo keeps; the
96
+ rows stay in your database. Skipping the push makes every write to a new or
97
+ changed model fail with `server_execute_unknown_model` — that error literally
98
+ means "run `npx ablo push`."
99
+
100
+ ## 5. Write through the model
101
+
102
+ The rows land in your Postgres; every connected client sees them live.
103
+
104
+ ```ts
105
+ import { ablo } from './ablo/client';
106
+
41
107
  await ablo.ready();
42
108
 
43
109
  const created = await ablo.weatherReports.create({
44
- data: {
45
- location: 'Stockholm',
46
- status: 'pending',
47
- },
110
+ data: { location: 'Stockholm', status: 'pending' },
48
111
  });
49
112
 
50
113
  const updated = await ablo.weatherReports.update({
51
114
  id: created.id,
52
- data: {
53
- status: 'ready',
54
- forecast: 'Light rain, 13C',
55
- },
115
+ data: { status: 'ready', forecast: 'Light rain, 13C' },
56
116
  });
57
117
 
58
- console.log({ id: updated.id, status: updated.status });
59
- ```
60
-
61
- Expected output:
62
-
63
- ```txt
64
- { id: '...', status: 'ready' }
65
- ```
66
-
67
- ## Run the example
68
-
69
- ```bash
70
- cd packages/sync-engine
71
- ABLO_API_KEY=sk_test_... npx tsx examples/quickstart.ts
118
+ console.log({ id: updated.id, status: updated.status }); // { id: '...', status: 'ready' }
72
119
  ```
73
120
 
74
121
  ## Add coordination for slow work
@@ -146,5 +193,5 @@ Keep using the schema client for app and agent writes.
146
193
  - [Schema Contract](./schema-contract.md) explains what the schema drives across SDK, React, agents, Data Source, and schema push.
147
194
  - [Guarantees](./guarantees.md) explains what confirmed writes and stale checks mean.
148
195
  - [Client Behavior](./client-behavior.md) covers errors, retries, and public imports.
149
- - [Connect Your Database](./data-sources.md) covers the optional route for teams keeping rows in their own database.
196
+ - [Connect Your Database](./data-sources.md) covers both connection shapes `databaseUrl` and the signed Data Source endpoint.
150
197
  - [AI SDK Tool](./examples/ai-sdk-tool.md) shows the same write path inside a tool call.
package/docs/react.md CHANGED
@@ -14,6 +14,27 @@ The React bindings ship with the main package — no extra install.
14
14
  import { useAblo } from '@abloatai/ablo/react';
15
15
  ```
16
16
 
17
+ ## Building the client
18
+
19
+ You build the Ablo client once — that's where the schema, the session endpoint,
20
+ and connection config live — then hand it to the provider. The provider takes
21
+ the already-built `client`; it no longer takes `schema`, `url`, `apiKey`, etc.
22
+ as props. This mirrors Stripe's `<Elements stripe={stripePromise}>`: construct
23
+ the thing, then pass it.
24
+
25
+ ```ts
26
+ // lib/ablo.ts
27
+ import Ablo from '@abloatai/ablo';
28
+ import { schema } from '@/ablo/schema';
29
+
30
+ // The browser never holds your API key. It mints a short-lived session token
31
+ // from your own server route (see Identity below).
32
+ export const ablo = Ablo({
33
+ schema,
34
+ authEndpoint: '/api/ablo-session',
35
+ });
36
+ ```
37
+
17
38
  ## AbloProvider
18
39
 
19
40
  Mount it once near the root of your tree. It owns the connection, the local
@@ -23,48 +44,38 @@ pool, and the engine lifecycle; everything below it reads with `useAblo`.
23
44
  'use client';
24
45
 
25
46
  import { AbloProvider } from '@abloatai/ablo/react';
26
- import { schema } from '@/ablo/schema';
47
+ import { ablo } from '@/lib/ablo';
27
48
 
28
49
  export function Providers({
29
50
  children,
30
51
  user, // resolved server-side from YOUR auth
31
52
  }: {
32
53
  children: React.ReactNode;
33
- user: { id: string; teamIds: string[] };
54
+ user: { id: string };
34
55
  }) {
35
56
  return (
36
- <AbloProvider
37
- schema={schema}
38
- userId={user.id}
39
- teamIds={user.teamIds}
40
- fallback={<AppSkeleton />}
41
- >
57
+ <AbloProvider client={ablo} userId={user.id} fallback={<AppSkeleton />}>
42
58
  {children}
43
59
  </AbloProvider>
44
60
  );
45
61
  }
46
62
  ```
47
63
 
48
- `schema` is the only required prop. The rest are situational:
49
-
50
- | Prop | Default | Purpose |
51
- | ------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------- |
52
- | `schema` | — | **Required.** From `defineSchema()`. Determines the typed hook surface. |
53
- | `userId` | resolved from auth | App participant id for app-owned fields and your `identityRoles.extract`. Not the security boundary. |
54
- | `teamIds` | resolved from auth | Team ids expanded into team sync groups via `identityRoles`. |
55
- | `syncGroups` | full allowed set | **Narrows** the subscription to a subset of what auth allows (e.g. `['deck:abc123']`). Never widens it. |
56
- | `url` | hosted endpoint | WebSocket URL of the sync server (`wss://…`). Hosted apps omit it. |
57
- | `apiKey` | session/cookie | Bootstrap auth. Browser apps **omit this** the key stays server-side. See Identity below. |
58
- | `fallback` | neutral spinner | Rendered during the *first* bootstrap only. Pass a branded skeleton, `null`, or `'passthrough'`. |
59
- | `bootstrapMode` | `'full'` | `'full'` loads existing rows before the app renders, so reads are populated on first paint; `'none'` skips that initial load and only streams changes as they happen.|
60
- | `persistence` | `'volatile'` | `'indexeddb'` opts into a durable browser cache that survives reloads. |
61
- | `onSessionExpired` | — | Fired after the engine has already purged on a rejected session — use for redirect-to-sign-in. |
62
- | `onError` | — | Engine / WebSocket / `postBootstrap` errors. Wire to Sentry / Datadog. |
63
-
64
- Where `userId` / `teamIds` / `syncGroups` come from, and why the API key never
65
- reaches the browser, is the whole of
66
- [Identity & Sync Groups](./identity.md) — read that if it isn't obvious how org
67
- / team / user map to what a participant can see.
64
+ `client` is the only required prop. The rest are situational:
65
+
66
+ | Prop | Default | Purpose |
67
+ | ----------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
68
+ | `client` | — | **Required.** The `Ablo({ schema, authEndpoint })` instance. It carries the schema and connection config. |
69
+ | `userId` | resolved from auth | App participant id for app-owned fields and your `identityRoles`. Not the security boundary. |
70
+ | `fallback` | neutral spinner | Rendered during the *first* bootstrap only. Pass a branded skeleton, `null`, or `'passthrough'`. |
71
+ | `onError` | | Engine / WebSocket / bootstrap errors. Wire to Sentry / Datadog. |
72
+
73
+ Everything that used to be a provider prop`schema`, `url`, `apiKey`,
74
+ `teamIds`, `syncGroups`/`scope`, `persistence`, `bootstrapMode` now lives on
75
+ the `Ablo({ ... })` client you build before mounting the provider. Where the
76
+ identity comes from, and why the API key never reaches the browser, is the whole
77
+ of [Identity & Sync Groups](./identity.md) read that if it isn't obvious how
78
+ org / team / user map to what a participant can see.
68
79
 
69
80
  ## useAblo — model client
70
81
 
@@ -88,10 +88,8 @@ the fresh row. Reads stay open; only acting on the row serializes.
88
88
 
89
89
  ## Storage boundary
90
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.
91
+ Every schema model is backed by your own database through a Data Source — Ablo
92
+ coordinates each write and your app commits it to your Postgres.
95
93
 
96
94
  Do not pass a database URL to `Ablo(...)`. Trusted runtimes use `ABLO_API_KEY`.
97
95
  Browser code goes through `<AbloProvider>` or a scoped session route, never a raw
package/llms-full.txt ADDED
@@ -0,0 +1,360 @@
1
+ # Ablo Full Context
2
+
3
+ Ablo is the state coordination layer for apps where humans and agents edit the same data.
4
+
5
+ Use AI SDK for the agent loop. Use Ablo when agent reads and writes must persist to the database, coordinate with concurrent human or agent work, and leave an audit trail.
6
+
7
+ The product surface should read like Stripe, Turbopuffer, or Reducto: one product-name import, models by dot access, optional subpaths only for schema, React, and tests.
8
+
9
+ ```ts
10
+ import Ablo from '@abloatai/ablo';
11
+ ```
12
+
13
+ ## Public Surface
14
+
15
+ Public imports:
16
+
17
+ - `@abloatai/ablo` — `Ablo`, errors, typed model clients, claims, `dataSource`, and advanced schema-less protocol models.
18
+ - `@abloatai/ablo/schema` — `defineSchema`, `model`, `z`, relations, schema types.
19
+ - `@abloatai/ablo/react` — React provider and hooks.
20
+ - `@abloatai/ablo/testing` — test harnesses and mocks.
21
+
22
+ Do not teach `/api`, `/agent`, `/ai-sdk`, `/core`, `/realtime`, or `internal/*` as public imports. The Data Source surface — `/source`, `/source/next`, `/source/drizzle`, `/source/kysely`, `/source/conformance` — IS public (it's how a customer-owned database is wired).
23
+
24
+ The canonical integration doc is `integration-guide`. It explains the end-to-end
25
+ path for schema, React selectors, your Data Source-backed database, multiplayer,
26
+ and agent workers. Use it before inventing a new setup
27
+ flow.
28
+
29
+ ## Production Guarantees
30
+
31
+ `wait: 'confirmed'` resolves after the server accepts the write. Schema model
32
+ writes are optimistic; a server rejection rolls back local state and raises a
33
+ typed `AbloError`.
34
+
35
+ Use `snapshot(...)` and `readAt` when an agent write depends on state it already
36
+ read. `onStale: 'reject'` prevents lost updates by rejecting if the target
37
+ changed after the snapshot.
38
+
39
+ Claims are live coordination signals, not database locks. Schema clients wait
40
+ from the realtime claim stream. Schema-less HTTP clients must pass an explicit
41
+ `claimedPollInterval` for `ifClaimed: 'wait'`; no hidden hard-coded polling.
42
+
43
+ Agent run bookkeeping is internal. Most users should not create run ledger
44
+ records or scoped access credentials manually.
45
+
46
+ ## Primary App Path
47
+
48
+ Declare models in a schema, then use typed model clients:
49
+
50
+ ```ts
51
+ import Ablo from '@abloatai/ablo';
52
+ import { defineSchema, model, z } from '@abloatai/ablo/schema';
53
+
54
+ const schema = defineSchema({
55
+ weatherReports: model({
56
+ id: z.string(),
57
+ location: z.string(),
58
+ status: z.enum(['pending', 'ready']),
59
+ forecast: z.string().optional(),
60
+ updatedAt: z.string(),
61
+ }),
62
+ projects: model({
63
+ id: z.string(),
64
+ name: z.string(),
65
+ }),
66
+ });
67
+
68
+ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
69
+
70
+ const report = await ablo.weatherReports.retrieve({ id: 'report_stockholm' });
71
+ if (!report) throw new Error('Row not found');
72
+
73
+ await using claim = await ablo.weatherReports.claim({ id: 'report_stockholm' });
74
+ const updated = await ablo.weatherReports.update({
75
+ id: claim.data.id,
76
+ data: { status: 'ready', forecast: await getForecast(claim.data) },
77
+ wait: 'confirmed',
78
+ });
79
+ ```
80
+
81
+ The normal app API (every verb takes ONE options object):
82
+
83
+ - `ablo.<model>.retrieve({ id })` — async read of one row from the server
84
+ - `ablo.<model>.list({ where })` — async read of many from the server
85
+ - `ablo.<model>.get(id)` / `getAll({ where })` / `getCount({ where })` — synchronous local-graph reads (reactive in React render)
86
+ - `ablo.<model>.create({ data, id? })`
87
+ - `ablo.<model>.update({ id, data })`
88
+ - `ablo.<model>.delete({ id })`
89
+ - `await using claim = await ablo.<model>.claim({ id })` — disposable claim handle; read the fresh row off `claim.data`; auto-releases on scope exit
90
+ - `ablo.<model>.claim.state({ id })` / `claim.queue({ id })` / `claim.release({ id })` / `claim.reorder({ id, order })`
91
+
92
+ The synchronous reads (`getAll`/`getCount`) accept `where`, `filter`, `orderBy`,
93
+ `limit`, `offset`, and `state`; state defaults to `'live'`, with `'archived'`
94
+ and `'all'` for lifecycle-aware reads.
95
+
96
+ Use `ablo.model<T>(name)` only when the caller intentionally does not have a schema, such as custom server agents, MCP routes, migration scripts, or generic admin tools.
97
+
98
+ React reads should use selector `useAblo`:
99
+
100
+ ```tsx
101
+ const report = useAblo((ablo) => ablo.weatherReports.get(id)) ?? serverReport;
102
+ const visibleReports = useAblo((ablo) =>
103
+ ablo.weatherReports.getAll({ where: { projectId } }),
104
+ );
105
+ ```
106
+
107
+ Use zero-argument `useAblo()` only for callbacks, effects, and writes that need
108
+ the provider-owned client:
109
+
110
+ ```tsx
111
+ const ablo = useAblo();
112
+ await ablo?.weatherReports.update({ id, data: patch, wait: 'confirmed' });
113
+ ```
114
+
115
+ Treat `useQuery`, `useOne`, `useReader`, and `useMutate` as compatibility hooks
116
+ for older string-keyed integrations. Do not teach them as the first React API.
117
+
118
+ ## Multiplayer
119
+
120
+ Multiplayer is an effect of the normal model API, not a separate setup path.
121
+ When human UI, server actions, and agents use the same
122
+ `Ablo({ schema, apiKey })` client and write through `ablo.<model>`, Ablo
123
+ coordinates the shared model stream:
124
+
125
+ - confirmed model writes fan out as realtime deltas,
126
+ - React hooks read the latest row and active claims,
127
+ - claims announce active work before a commit,
128
+ - `snapshot(...)` plus `readAt` protects against stale writes.
129
+
130
+ Direct database writes outside Ablo are not coordinated until the app reports
131
+ them through Data Source events. In Data Source mode, writes made through Ablo
132
+ still coordinate normally because Ablo sends the signed commit request, receives
133
+ canonical rows, and fans out the resulting deltas.
134
+
135
+ For existing Python backends, keep the Python service layer and database as the
136
+ source of truth. Add one signed Data Source endpoint, keep initial page loads on
137
+ existing APIs if needed, use `useAblo((ablo) => ablo.<model>.get(id)) ?? serverRow`
138
+ for live rows, then migrate buttons one at a time from direct Python endpoint
139
+ writes to `ablo.<model>.update(...)`.
140
+
141
+ This applies to any API-backed app: Python, Rails, Go, or Node. The backend keeps
142
+ its service layer and DB credentials. Ablo gets a Data Source endpoint and uses
143
+ `ABLO_API_KEY`, not a database URL.
144
+
145
+ ## Client Behavior
146
+
147
+ Important options: `schema`, `apiKey`, `baseURL`, `persistence`, `fetch`,
148
+ `defaultHeaders`, `defaultQuery`, `logger`, and `dangerouslyAllowBrowser`.
149
+
150
+ There is intentionally no `databaseURL` option on `Ablo(...)`. Application and
151
+ agent code use `ABLO_API_KEY`. Customer-owned app databases stay private behind
152
+ a signed Data Source endpoint.
153
+
154
+ Important per-write options: `wait`, `readAt`, `onStale`,
155
+ `idempotencyKey`, and `timeout`.
156
+
157
+ Errors: `AbloAuthenticationError`, `AbloPermissionError`, `AbloRateLimitError`,
158
+ `AbloIdempotencyError`, `AbloConnectionError`, `AbloValidationError`,
159
+ `AbloServerError`, `AbloStaleContextError`, and `AbloClaimedError`.
160
+
161
+ Retry transport failures and 5xx with backoff only when the operation is
162
+ idempotent. Do not blindly retry validation, permission, idempotency, claimed, or
163
+ stale-context errors without changing the request.
164
+
165
+ ## Sandboxes
166
+
167
+ There are two sandbox surfaces:
168
+
169
+ - Public `/sandbox` is a deterministic visual playground. It demonstrates shared
170
+ state, active claims, stale-write rejection, receipts, and deltas without
171
+ making real Ablo calls. It is agent-first: it should expose a prompt that can
172
+ be pasted into Claude Code or Codex to wire one real model through Ablo.
173
+ - Authenticated org sandboxes are real test environments. The default sandbox is
174
+ the Stripe-style test mode for an org. It has an isolated sync group prefix,
175
+ can mint `sk_test_*` keys, and can be reset without touching live state.
176
+
177
+ Additional org sandboxes can start blank or copy live configuration. Keep
178
+ customer production traffic on `sk_live_*`; use `sk_test_*` for app setup, Data
179
+ Source endpoint wiring, and agent integration testing.
180
+
181
+ The coding-agent handoff should ask for a narrow integration: one schema model,
182
+ one Ablo client module, one React selector read, one typed model write with
183
+ `readAt`/`onStale: 'reject'`/`wait: 'confirmed'`, and one smoke test where two
184
+ writers prove claim/stale behavior.
185
+
186
+ ## Data Sources
187
+
188
+ Use these public environment names:
189
+
190
+ - `ABLO_API_KEY` — SDK authentication for app and agent code. Where it comes
191
+ from: the human runs `npx ablo login` once (browser; an agent must not run
192
+ it), and `npx ablo dev` then writes `ABLO_API_KEY=sk_test_…` into
193
+ `.env.local` automatically. Check the environment and `.env.local` before
194
+ asking the human for a key.
195
+
196
+ Do not ask customers to paste their app database URL into Ablo. If their app
197
+ database is canonical, they expose a Data Source endpoint and keep database
198
+ credentials inside their app.
199
+
200
+ Every schema model is backed by the customer's own database. The SDK methods
201
+ `ablo.<model>.create/update/delete` produce a signed commit request to the
202
+ customer's Data Source route, and that route writes the app database.
203
+
204
+ When the customer's database is canonical, expose one signed source route:
205
+
206
+ ```ts
207
+ import { dataSourceNext } from '@abloatai/ablo/source/next';
208
+ import { prismaDataSource } from '@abloatai/ablo/source';
209
+ import { schema } from './ablo.schema';
210
+ import { prisma } from './lib/prisma';
211
+
212
+ // The adapter owns commit / idempotency / outbox — no hand-written commit.
213
+ export const { POST } = dataSourceNext({
214
+ schema,
215
+ apiKey: process.env.ABLO_API_KEY!,
216
+ adapter: prismaDataSource(prisma, schema), // or drizzleDataSource(db, schema) / kyselyDataSource(db, schema)
217
+ });
218
+ ```
219
+
220
+ Commit request shape:
221
+
222
+ ```ts
223
+ {
224
+ type: 'commit',
225
+ clientTxId: 'tx_...',
226
+ operations: [
227
+ { type: 'UPDATE', model: 'weatherReports', id: 'report_stockholm', input, readAt, onStale: 'reject' },
228
+ ],
229
+ scope: { participantId, participantKind, organizationId, requiredSyncGroups, mode: 'live' },
230
+ }
231
+ ```
232
+
233
+ Return `{ rows }` for normal apps or `{ deltas }` for sources that already
234
+ compute canonical change events. External writes come back through the source
235
+ `events` handler or `POST /api/source/events`.
236
+
237
+ ## Advanced Schema Options
238
+
239
+ Teach schema as model fields and relations first:
240
+
241
+ ```ts
242
+ const schema = defineSchema({
243
+ weatherReports: model({
244
+ id: z.string(),
245
+ location: z.string(),
246
+ }),
247
+ });
248
+ ```
249
+
250
+ Advanced helpers such as `mutable`, `readOnly`, `field`, `indexed`, query definitions, `load` strategies, parent metadata, and sync-group formats are still part of the schema package for offline/cache/indexing-heavy apps. Do not put them in the first example unless the user asks for local persistence, offline sync, custom loading, or low-level indexing behavior.
251
+
252
+ ## Server Agent Path
253
+
254
+ Server agents should import the same schema as the app and use the same model
255
+ methods. Claim the row around slow work, then write through `ablo.<model>`.
256
+
257
+ ```ts
258
+ import Ablo from '@abloatai/ablo';
259
+
260
+ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
261
+
262
+ await using claim = await ablo.weatherReports.claim({ id: 'report_stockholm' });
263
+ const forecast = await getForecast(claim.data);
264
+ await ablo.weatherReports.update({
265
+ id: claim.data.id,
266
+ data: { status: 'ready', forecast },
267
+ wait: 'confirmed',
268
+ });
269
+ ```
270
+
271
+ Do not require users to manually create protocol bookkeeping objects in the
272
+ common agent path.
273
+
274
+ ## Model
275
+
276
+ Every model read returns state plus coordination metadata:
277
+
278
+ ```ts
279
+ type ModelRead<T> = {
280
+ data: T;
281
+ stamp: number;
282
+ claims: ModelClaim[];
283
+ };
284
+ ```
285
+
286
+ `stamp` is the state watermark. Pass it to writes as `readAt`.
287
+
288
+ `claims` lists active work on the target. Reads are allowed while another participant is working. The caller decides whether to return, fail, or wait.
289
+
290
+ ## Claimed Behavior
291
+
292
+ Claimed behavior is explicit:
293
+
294
+ - `ifClaimed: 'return'` returns the current claims with the read.
295
+ - `ifClaimed: 'fail'` throws `AbloClaimedError`.
296
+ - `ifClaimed: 'wait'` waits for matching claims to clear.
297
+
298
+ Schema clients use the realtime claim stream for waits.
299
+
300
+ Schema-less HTTP clients cannot know when a claim clears unless the caller opts into polling. When using `ifClaimed: 'wait'` over HTTP, provide `claimedPollInterval` and usually `claimedTimeout`.
301
+
302
+ ```ts
303
+ await api.model('reports').retrieve({
304
+ id: 'report_stockholm',
305
+ ifClaimed: 'wait',
306
+ claimedPollInterval: 1_000,
307
+ claimedTimeout: 30_000,
308
+ });
309
+ ```
310
+
311
+ No hidden hard-coded claimed polling. `claimedTimeout` is a maximum wait, not the coordination mechanism.
312
+
313
+ ## Write
314
+
315
+ Use model methods as the normal write path:
316
+
317
+ ```ts
318
+ const snap = ablo.snapshot({ weatherReports: 'report_stockholm' });
319
+ const updated = await ablo.weatherReports.update({
320
+ id: 'report_stockholm',
321
+ data: { status: 'ready' },
322
+ readAt: snap.stamp,
323
+ onStale: 'reject',
324
+ wait: 'confirmed',
325
+ });
326
+ ```
327
+
328
+ Write checks:
329
+
330
+ - authorization
331
+ - stale state via `readAt`
332
+ - active claim conflicts
333
+ - idempotency when an idempotency key is provided
334
+
335
+ Use `commits.create(...)` only for low-level batch writes.
336
+
337
+ ## Receipt
338
+
339
+ Schema model writes return the updated row. Passing `wait: 'confirmed'` waits for
340
+ the server confirmation before resolving. Lower-level model writes return a
341
+ receipt object for custom runtimes that need protocol-level proof.
342
+
343
+ ## Interaction Model
344
+
345
+ The common agent run is:
346
+
347
+ 1. Read the row through `ablo.<model>.retrieve({ id })` (or `list({ where })`).
348
+ 2. Claim it: `await using claim = await ablo.<model>.claim({ id })` — waits if held, hands you `claim.data`.
349
+ 3. Do the slow work from `claim.data`.
350
+ 4. Write with `ablo.<model>.update({ id, data })`.
351
+ 5. The claim auto-releases when it goes out of scope (`await using`).
352
+
353
+ The underlying primitives are Model, Claim, Commit, and Receipt. Most schema
354
+ users should not call `commits.create(...)` directly.
355
+
356
+ ## Public HTTP Routes
357
+
358
+ - `GET /v1/models/{model}/{id}` — read state plus active claims
359
+ - `POST /v1/commits` — apply mutations
360
+ Use `Authorization: Bearer <api key>` for platform calls.