@abloatai/ablo 0.7.0 → 0.8.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 (83) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +54 -45
  3. package/dist/BaseSyncedStore.js +7 -3
  4. package/dist/SyncEngineContext.d.ts +2 -1
  5. package/dist/SyncEngineContext.js +5 -3
  6. package/dist/agent/session.js +3 -2
  7. package/dist/auth/index.js +39 -11
  8. package/dist/client/Ablo.d.ts +111 -3
  9. package/dist/client/Ablo.js +143 -10
  10. package/dist/client/ApiClient.d.ts +32 -0
  11. package/dist/client/ApiClient.js +76 -44
  12. package/dist/client/auth.d.ts +11 -1
  13. package/dist/client/auth.js +21 -2
  14. package/dist/client/createModelProxy.d.ts +107 -63
  15. package/dist/client/createModelProxy.js +65 -33
  16. package/dist/client/identity.js +14 -0
  17. package/dist/client/registerDataSource.d.ts +19 -0
  18. package/dist/client/registerDataSource.js +57 -0
  19. package/dist/client/validateAbloOptions.d.ts +2 -1
  20. package/dist/client/validateAbloOptions.js +8 -7
  21. package/dist/errorCodes.d.ts +23 -1
  22. package/dist/errorCodes.js +34 -1
  23. package/dist/errors.d.ts +52 -1
  24. package/dist/errors.js +140 -42
  25. package/dist/index.d.ts +9 -5
  26. package/dist/index.js +9 -5
  27. package/dist/keys/index.d.ts +61 -0
  28. package/dist/keys/index.js +151 -0
  29. package/dist/query/client.js +19 -8
  30. package/dist/react/AbloProvider.d.ts +25 -0
  31. package/dist/react/AbloProvider.js +97 -2
  32. package/dist/react/ClientSideSuspense.d.ts +1 -1
  33. package/dist/react/DefaultFallback.d.ts +1 -1
  34. package/dist/react/SyncGroupProvider.d.ts +1 -1
  35. package/dist/react/index.d.ts +3 -2
  36. package/dist/react/index.js +3 -2
  37. package/dist/react/useAblo.d.ts +4 -4
  38. package/dist/react/useAblo.js +10 -5
  39. package/dist/react/useReactive.js +16 -3
  40. package/dist/schema/serialize.d.ts +3 -3
  41. package/dist/schema/serialize.js +2 -2
  42. package/dist/sync/BootstrapHelper.js +46 -27
  43. package/dist/sync/ConnectionManager.d.ts +3 -1
  44. package/dist/sync/ConnectionManager.js +37 -1
  45. package/dist/sync/HydrationCoordinator.js +3 -2
  46. package/dist/sync/NetworkProbe.d.ts +8 -0
  47. package/dist/sync/NetworkProbe.js +24 -2
  48. package/dist/sync/SyncWebSocket.d.ts +1 -1
  49. package/dist/sync/SyncWebSocket.js +43 -53
  50. package/dist/sync/participants.js +5 -2
  51. package/dist/transactions/TransactionQueue.js +13 -1
  52. package/docs/api-keys.md +5 -5
  53. package/docs/api.md +101 -44
  54. package/docs/audit.md +16 -9
  55. package/docs/cli.md +27 -17
  56. package/docs/client-behavior.md +34 -20
  57. package/docs/coordination.md +40 -51
  58. package/docs/data-sources.md +21 -19
  59. package/docs/examples/agent-human.md +72 -28
  60. package/docs/examples/ai-sdk-tool.md +14 -11
  61. package/docs/examples/existing-python-backend.md +27 -16
  62. package/docs/examples/nextjs.md +21 -8
  63. package/docs/examples/scoped-agent.md +42 -27
  64. package/docs/examples/server-agent.md +27 -5
  65. package/docs/guarantees.md +26 -17
  66. package/docs/identity.md +65 -59
  67. package/docs/index.md +30 -19
  68. package/docs/integration-guide.md +52 -52
  69. package/docs/interaction-model.md +38 -26
  70. package/docs/mcp/claude-code.md +9 -17
  71. package/docs/mcp/cursor.md +6 -24
  72. package/docs/mcp/windsurf.md +6 -19
  73. package/docs/mcp.md +103 -26
  74. package/docs/quickstart.md +31 -39
  75. package/docs/react.md +15 -11
  76. package/docs/roadmap.md +13 -13
  77. package/docs/schema-contract.md +109 -0
  78. package/examples/README.md +8 -4
  79. package/examples/data-source/README.md +6 -2
  80. package/examples/data-source/run.ts +4 -3
  81. package/examples/quickstart.ts +1 -1
  82. package/llms.txt +27 -16
  83. package/package.json +6 -1
@@ -16,31 +16,18 @@ Add the Ablo Sync MCP server to Windsurf's MCP config:
16
16
  ```
17
17
 
18
18
  The config path differs by platform — Windsurf surfaces it in Settings →
19
- Cascade → MCP. Restart Windsurf after saving.
20
-
21
- ## With auth
22
-
23
- ```json
24
- {
25
- "mcpServers": {
26
- "ablo-sync": {
27
- "transport": "http",
28
- "url": "https://<your-app>/api/mcp",
29
- "headers": {
30
- "Authorization": "Bearer $ABLO_MCP_TOKEN"
31
- }
32
- }
33
- }
34
- }
35
- ```
19
+ Cascade → MCP. Restart Windsurf after saving. No auth header is needed —
20
+ the endpoint is public and serves only docs, schema lint, and scaffolds.
36
21
 
37
22
  ## Verify
38
23
 
39
24
  Cascade's MCP panel lists every configured server with its tools. You
40
- should see `ablo-sync` with model tools enumerated.
25
+ should see `ablo-sync` with the integration tools enumerated:
26
+ `search_ablo_docs`, `get_recipe`, `get_api_surface`, `validate_schema`,
27
+ `scaffold_app`.
41
28
 
42
29
  ## More
43
30
 
44
- - [MCP overview](/docs/mcp) — how the transport works.
31
+ - [MCP overview](/docs/mcp) — what the server exposes and how the transport works.
45
32
  - [Claude Code setup](/docs/mcp/claude-code) — CLI install.
46
33
  - [Cursor setup](/docs/mcp/cursor) — same JSON shape.
package/docs/mcp.md CHANGED
@@ -1,44 +1,121 @@
1
1
  # Model Context Protocol
2
2
 
3
- Ablo ships an MCP server at `/api/mcp`. Connect any MCP-compatible AI
4
- assistant — Claude Code, Cursor, Windsurf — and your sync models become
5
- typed, callable tools.
3
+ Ablo publishes **two** MCP servers for two different jobs. Don't confuse them:
6
4
 
7
- ## Install
5
+ | Server | Purpose | Auth | Tools |
6
+ |---|---|---|---|
7
+ | **Coordination** (`@ablo/mcp`) | Let an agent safely read & mutate your application data | API key (`sk_…` / `rk_…`) | per-model `get` / `list` / `create` / `update` / `delete` / `claim` / `release` |
8
+ | **Integration-helper** (hosted `/api/mcp`) | Help an AI coding assistant write SDK integration code that compiles | none (public docs) | doc search, export surface, schema lint, scaffold |
8
9
 
9
- Pick your client:
10
+ The coordination server **is the data plane** — it is how an agent changes
11
+ state. The integration-helper server only serves docs, schema lint, and
12
+ scaffolds; it does **not** read or write application data (there are no
13
+ per-model data tools on it). Pick by what you're doing: shipping an agent that
14
+ edits rows → coordination; teaching your IDE assistant the SDK → helper.
15
+
16
+ ## Coordination server (`@ablo/mcp`)
17
+
18
+ The coordination server is the MCP projection of the model-scoped API
19
+ (`/v1/models/...`) — the same surface as `ablo.<model>.create/update/claim` and
20
+ the REST routes, rendered as tools. An agent connects with your API key and
21
+ gets one safe loop: **claim → read → commit → release.**
22
+
23
+ Install over stdio; set your key in the host's MCP env:
24
+
25
+ ```bash
26
+ claude mcp add ablo -- npx -y @ablo/mcp
27
+ # env: ABLO_API_KEY=sk_… (ABLO_API_URL optional; defaults to the hosted API)
28
+ ```
29
+
30
+ Each tool mirrors an SDK verb, scoped to a model + id:
31
+
32
+ | Tool | Mirrors | Does |
33
+ |---|---|---|
34
+ | `get_model` | `ablo.<model>.get(id)` | read latest state + active claims |
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 |
40
+ | `release_claim` | — | release the lease so others proceed |
41
+
42
+ The agent-facing contract — the safe loop, the "derive idempotency keys from
43
+ the business event" rule, and the error-code playbook — ships as a loadable
44
+ skill at `@ablo/mcp/skill.md`. Lives in `packages/mcp/`
45
+ (`createCoordinationMcpServer`, `src/tools.ts`).
46
+
47
+ ## Integration-helper server
48
+
49
+ If you're integrating `@abloatai/ablo` with the help of an AI coding
50
+ assistant (Claude Code, Cursor, Windsurf, Codex), you don't want it guessing
51
+ at the API. This hosted server lets the assistant search the real docs,
52
+ inspect the actual export surface, lint your schema, and scaffold a starter —
53
+ so the code it writes uses APIs that exist. It serves docs only and returns
54
+ nothing org-specific; data access happens through the SDK or the coordination
55
+ server above, never here.
56
+
57
+ > The `@abloatai/ablo` npm package itself bundles neither server — it has
58
+ > no `@modelcontextprotocol/sdk` dependency. The helper is a feature of Ablo's
59
+ > hosted app, mounted at `/api/mcp`; the coordination server is the separate
60
+ > `@ablo/mcp` package.
61
+
62
+ ### Install
63
+
64
+ Point your assistant at the hosted endpoint — no auth, no token:
65
+
66
+ ```bash
67
+ claude mcp add --transport http ablo-sync https://<your-app>/api/mcp
68
+ ```
69
+
70
+ Per-client walkthroughs:
10
71
 
11
72
  - [Claude Code](/docs/mcp/claude-code)
12
73
  - [Cursor](/docs/mcp/cursor)
13
74
  - [Windsurf](/docs/mcp/windsurf)
14
75
 
15
- ## How it works
76
+ ### What it exposes
16
77
 
17
- Each model you declare becomes one or more MCP tools:
78
+ #### Tools
18
79
 
19
- | Model method | MCP tool name | What it does |
20
- |---|---|---|
21
- | `retrieve` | `<model>.retrieve` | Returns the row + a stamp. |
22
- | `list` | `<model>.list` | Cursor-paginated discovery. |
23
- | `update` | `<model>.update` | Write, requires the prior stamp. |
24
- | `<model>.claim` | `claim.create` | Claim a row before writing, then release when held work finishes. |
80
+ | Tool | What it does |
81
+ |---|---|
82
+ | `search_ablo_docs` | Keyword search across the docs corpus. Returns ranked matches with excerpts. Follow up with `get_recipe` on the top hit. |
83
+ | `get_recipe` | Returns the full markdown of one doc by name (e.g. `readme`, `quickstart`, `schema-contract`, `integration-guide`, `api`, `guarantees`). |
84
+ | `get_api_surface` | Returns the structured export list for an SDK subpath (`@abloatai/ablo`, `./react`, `./schema`, `./testing`, …). Call with no argument to list every subpath. |
85
+ | `validate_schema` | Lints `defineSchema` source against the DSL rules (camelCase fields, lowercase model keys, `scope`/`grants` sync groups, valid `load` strategies, no legacy builders) and returns a structured issue list. Runs no code. |
86
+ | `scaffold_app` | Emits a starter file tree for a schema-first integration — `next`, `node-agent`, or `plain`, with `managed` or `data-source` storage. |
87
+
88
+ #### Resources
89
+
90
+ Every doc file is addressable at `ablo-sync-engine://docs/{name}`, so a
91
+ client can list the corpus and fetch individual files on demand instead of
92
+ loading everything into context.
93
+
94
+ #### Prompts
25
95
 
26
- The assistant gets typed JSON schemas, real argument types, and typed
27
- rejections when it writes stale state. No invention, no hallucinated IDs.
96
+ Reusable, parameterised templates that drive an end-to-end flow:
28
97
 
29
- ## Auth
98
+ - `integrate-sync-engine` — wire the SDK into an existing project.
99
+ - `add-agent` — add an agent worker that coordinates via intents and
100
+ conflict-safe writes.
101
+ - `define-schema` — design a Zod-first schema from a description, then run
102
+ `validate_schema` before committing.
30
103
 
31
- The MCP transport uses a scoped bearer token issued by your server. Pass that
32
- token into the MCP client's auth header configuration. See your client's setup
33
- guide for the exact mechanism.
104
+ ### Transport and limits
34
105
 
35
- ## Limits
106
+ The endpoint uses the stateless Streamable HTTP transport (`POST /api/mcp`;
107
+ `GET` returns 405 — SSE is not supported in stateless mode). A fresh
108
+ server is built per request, which suits serverless and horizontally-scaled
109
+ deployments.
36
110
 
37
- The MCP endpoint is rate-limited per token. Read-heavy bursts are fine;
38
- write-heavy bursts get throttled at the dashboard-configured cap.
111
+ There is **no authentication**: the server only serves docs, schema lint,
112
+ and scaffolds, so there's nothing org-scoped to protect. Abuse is bounded
113
+ by IP-based rate limiting — 120 requests per minute per IP. Rate-limit
114
+ headers are echoed on every response.
39
115
 
40
- ## More
116
+ ### Where it lives
41
117
 
42
- The [MCP landing page](/mcp) has the product pitch. The route handler
43
- itself is at `apps/sync-web/src/app/api/mcp/route.ts` if you want to read
44
- the implementation.
118
+ - Route handler: `apps/sync-web/src/app/api/mcp/route.ts`
119
+ - Server setup: `apps/sync-web/src/lib/mcp-ablo/server.ts`
120
+ (`createSyncEngineMcpServer`)
121
+ - Tools: `apps/sync-web/src/lib/mcp-ablo/tools/`
@@ -1,29 +1,26 @@
1
1
  # Quickstart
2
2
 
3
- Declare your state, create one row, and make one confirmed update.
3
+ Start building with Ablo in two steps: install, then declare one model and write
4
+ through its generated client.
4
5
 
5
6
  If you already have a backend and database, still start here. The SDK call shape
6
7
  is the same; [Integration Guide](./integration-guide.md) explains when to use
7
8
  Ablo-managed state versus a Data Source that calls your existing API service.
8
9
 
9
- ## 1. Install
10
+ ## 1. Install and set a sandbox key
10
11
 
11
12
  ```bash
12
13
  npm install @abloatai/ablo
13
- ```
14
-
15
- ## 2. Set a Sandbox Key
16
-
17
- Use an Ablo sandbox key while integrating.
18
-
19
- ```bash
20
14
  export ABLO_API_KEY=sk_test_...
21
15
  ```
22
16
 
23
17
  `ABLO_API_KEY` is for trusted server runtimes. Browser apps should use the React
24
18
  provider with a scoped session route, not a bundled API key.
25
19
 
26
- ## 3. Declare a Schema
20
+ ## 2. Declare schema and write state
21
+
22
+ Your schema is the contract. It generates `ablo.<model>` methods for app code,
23
+ server actions, agents, React reads, and Data Source requests.
27
24
 
28
25
  ```ts
29
26
  import Ablo from '@abloatai/ablo';
@@ -41,15 +38,6 @@ export const ablo = Ablo({
41
38
  schema,
42
39
  apiKey: process.env.ABLO_API_KEY,
43
40
  });
44
- ```
45
-
46
- Customer apps should always pass `schema`. Treat it like Prisma's schema file:
47
- it is the source of truth for typed model clients, realtime subscriptions,
48
- agent writes, and Data Source requests.
49
-
50
- ## 4. Create and Update
51
-
52
- ```ts
53
41
  await ablo.ready();
54
42
 
55
43
  const created = await ablo.weatherReports.create({
@@ -71,23 +59,26 @@ Expected output:
71
59
  { id: '...', status: 'ready' }
72
60
  ```
73
61
 
74
- ## 5. Run the Example
62
+ ## Run the example
75
63
 
76
64
  ```bash
77
- cd examples
78
- ABLO_API_KEY=sk_test_... npx tsx quickstart.ts
65
+ cd packages/sync-engine
66
+ ABLO_API_KEY=sk_test_... npx tsx examples/quickstart.ts
79
67
  ```
80
68
 
81
- ## 6. AI Activity on Existing State
69
+ ## Add coordination for slow work
82
70
 
83
71
  When AI or background work will touch an existing row for more than a quick
84
- write, coordinate through the flat model verbs: `ablo.<model>.claim(id, ...)` to
85
- claim the row (returns the row), `ablo.<model>.claimState(id)` to read who's working
86
- on it (synchronous; never blocks), and the normal `ablo.<model>.update(id, ...)`
87
- to write. Normal reads still work while the claim is held; server reads can opt
88
- into `ifClaimed: 'wait'` or `ifClaimed: 'fail'` when they should not read through
89
- active work. The callback form releases the claim when the callback returns or
90
- throws.
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, ...)`.
75
+
76
+ Claims don't lock. If another writer holds the row, `claim` waits for them,
77
+ re-reads the fresh row, then hands it to you so two writers serialize instead
78
+ of clobbering. Normal reads still work while the claim is held. If a server read
79
+ 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.
91
82
 
92
83
  ```ts
93
84
  // Claim the row so other participants serialize behind us while we work.
@@ -107,24 +98,24 @@ await ablo.weatherReports.claim(
107
98
  );
108
99
  ```
109
100
 
110
- Ablo does not fetch the weather. The claim is **advisory**: if another
111
- participant already holds the row, `claim` waits for them to finish and re-reads
112
- before handing back the row. While you hold the claim, `update(id, ...)` is
113
- stale-guarded and rejects with `AbloStaleContextError` if the row changed under
114
- you. The claim releases automatically once the callback returns or throws.
101
+ Ablo does not fetch the weather. If another participant already holds the row,
102
+ `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`
104
+ 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
106
 
116
- ## 7. Multiplayer and Claimed Work
107
+ ## Multiplayer and claimed work
117
108
 
118
109
  There is no separate multiplayer mode. Use the same schema client for human UI,
119
110
  server actions, and agents; Ablo fans out confirmed writes and keeps active
120
111
  claims visible on the same model row.
121
112
 
122
- `claimState(id)` tells you when another human or agent is active on the same row.
113
+ `claim.state(id)` tells you when another human or agent is active on the same row.
123
114
  For schema clients, `claim(id, work)` waits fairly, re-reads, and then lets you
124
115
  write through the model.
125
116
 
126
117
  ```ts
127
- const active = ablo.weatherReports.claimState('weather_stockholm');
118
+ const active = ablo.weatherReports.claim.state('weather_stockholm');
128
119
  if (active) {
129
120
  console.log(`${active.heldBy} is ${active.action}`);
130
121
  }
@@ -137,11 +128,12 @@ await ablo.weatherReports.claim('weather_stockholm', async (report) => {
137
128
  Use `{ wait: false }` on `claim` when work should be skipped instead of queued
138
129
  behind an active holder.
139
130
 
140
- ## 8. Next Steps
131
+ ## Next steps
141
132
 
142
133
  Keep using the schema client for app and agent writes.
143
134
 
144
135
  - [Integration Guide](./integration-guide.md) explains the full app, React, Data Source, multiplayer, and agent path.
136
+ - [Schema Contract](./schema-contract.md) explains what the schema drives across SDK, React, agents, Data Source, and schema push.
145
137
  - [Guarantees](./guarantees.md) explains what confirmed writes and stale checks mean.
146
138
  - [Client Behavior](./client-behavior.md) covers errors, retries, and public imports.
147
139
  - [Connect Your Database](./data-sources.md) covers the optional route for teams keeping rows in their own database.
package/docs/react.md CHANGED
@@ -56,7 +56,7 @@ export function Providers({
56
56
  | `url` | hosted endpoint | WebSocket URL of the sync server (`wss://…`). Hosted apps omit it. |
57
57
  | `apiKey` | session/cookie | Bootstrap auth. Browser apps **omit this** — the key stays server-side. See Identity below. |
58
58
  | `fallback` | neutral spinner | Rendered during the *first* bootstrap only. Pass a branded skeleton, `null`, or `'passthrough'`. |
59
- | `bootstrapMode` | `'full'` | `'full'` pulls the org's baseline before ready; `'none'` skips the baseline and processes live deltas only.|
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
60
  | `persistence` | `'volatile'` | `'indexeddb'` opts into a durable browser cache that survives reloads. |
61
61
  | `onSessionExpired` | — | Fired after the engine has already purged on a rejected session — use for redirect-to-sign-in. |
62
62
  | `onError` | — | Engine / WebSocket / `postBootstrap` errors. Wire to Sentry / Datadog. |
@@ -74,8 +74,8 @@ reaches the browser, is the whole of
74
74
  import { useAblo } from '@abloatai/ablo/react';
75
75
 
76
76
  export function ReportView({ report: serverReport }: { report: { id: string; location: string } }) {
77
- const report = useAblo((ablo) => ablo.weatherReports.retrieve(serverReport.id)) ?? serverReport;
78
- const active = useAblo((ablo) => ablo.weatherReports.claimState(serverReport.id));
77
+ const report = useAblo((ablo) => ablo.weatherReports.get(serverReport.id)) ?? serverReport;
78
+ const active = useAblo((ablo) => ablo.weatherReports.claim.state(serverReport.id));
79
79
  const claimed = Boolean(active);
80
80
 
81
81
  return <article>{report.location}</article>;
@@ -84,12 +84,13 @@ export function ReportView({ report: serverReport }: { report: { id: string; loc
84
84
 
85
85
  The hook:
86
86
 
87
- 1. Reads through the same `ablo.<model>` methods as the rest of the SDK.
87
+ 1. Uses the same `ablo.<model>.get(id)` / `.getAll()` methods you'd call anywhere
88
+ else in the SDK — the hook just makes them reactive.
88
89
  2. Tracks the model fields read by the selector and re-renders when confirmed
89
90
  deltas arrive.
90
91
  3. Lets Server Component data stay outside the hook: use `?? serverReport` when a
91
92
  parent already loaded the row.
92
- 4. Works for coordination state too, such as `ablo.weatherReports.claimState(id)`.
93
+ 4. Works for coordination state too, such as `ablo.weatherReports.claim.state(id)`.
93
94
 
94
95
  Use the zero-argument form only when you need the full client for callbacks,
95
96
  effects, or writes:
@@ -98,15 +99,14 @@ effects, or writes:
98
99
  const abloClient = useAblo();
99
100
  ```
100
101
 
101
- Prefer selector reads like `useAblo((ablo) => ablo.<model>.retrieve(id))`.
102
- String model names are kept on older hooks for compatibility, but first examples
103
- should use the same model-client shape as the rest of the SDK.
102
+ Prefer selector reads like `useAblo((ablo) => ablo.<model>.get(id))`. Older hooks
103
+ also accept a string model name; prefer the selector form shown above.
104
104
 
105
105
  For collections, keep the selector on the model client too:
106
106
 
107
107
  ```tsx
108
108
  const reports = useAblo((ablo) =>
109
- ablo.weatherReports.list({
109
+ ablo.weatherReports.getAll({
110
110
  where: { projectId },
111
111
  filter: (report) => report.status !== 'ready',
112
112
  state: 'live',
@@ -117,10 +117,14 @@ const reports = useAblo((ablo) =>
117
117
  ## Server Load
118
118
 
119
119
  ```tsx
120
- const [report] = await ablo.weatherReports.load({ where: { id } });
120
+ const report = await ablo.weatherReports.retrieve(id);
121
121
  ```
122
122
 
123
- Use `load` in Server Components when the row may not be in the local pool yet.
123
+ Use `retrieve` in Server Components when the row may not be in the local pool
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
126
+ `list({ where })` for many; both are async. The synchronous local reads are
127
+ `get`/`getAll`/`getCount`, used in render below.)
124
128
 
125
129
  ## Writes
126
130
 
package/docs/roadmap.md CHANGED
@@ -5,22 +5,22 @@ What is shipped, what is next, and what we will not build.
5
5
  ## Shipped
6
6
 
7
7
  - **Models, claims, commits** — the core API.
8
- - **Audit log** — hash-chained per principal, with `delegationChainRoot`.
8
+ - **Audit log** — a tamper-evident, hash-chained record of who did what,
9
+ per principal.
9
10
  - **MCP transport** — HTTP server at `/api/mcp`.
10
11
  - **TypeScript SDK** — `@abloatai/ablo`, with React bindings.
11
12
  - **Dashboard** — keys, audit, metrics, allowed origins.
12
- - **Schema migrations** — declarative model schema changes. Pure
13
- diff/classify/cast-safety/backfill planning engine (`diffSchema`,
14
- `classifyMigration`, `classifyCast`, `isAutoApplicable`) ships in the SDK;
15
- `ablo generate` emits typed clients from a pushed schema; the server
16
- applies and activates migrations only on success.
17
- - **Structured error contract** versioned (`ERROR_CONTRACT_VERSION`),
18
- drift-guarded code registry shared across the HTTP, WebSocket, and MCP
19
- planes, with always-on request ids and an OpenAPI error envelope.
20
- - **Relation-driven sync groups** — membership-routed fan-out via branded
21
- `SyncGroup` + schema-declared identity roles (schema-JSON v2).
22
- - **Intent coordination plane** — Stripe-shaped `Intent` with per-model
23
- `intent(id)` handles for lease/await/commit coordination between agents.
13
+ - **Schema migrations** — change a model's shape after it already has data.
14
+ Ablo plans the migration, tells you which changes are safe to auto-apply,
15
+ and backfills the rest; the server applies and activates a migration only
16
+ if it succeeds. `ablo generate` emits typed clients from a pushed schema.
17
+ - **Structured errors** every error has a stable code and a request id,
18
+ and the same codes work whether you reach Ablo over HTTP, WebSocket, or MCP.
19
+ - **Sync groups** clients automatically receive only the records they have
20
+ access to, based on relationships you declare in the schema.
21
+ - **Agent coordination** — when several agents work on the same model, they
22
+ take turns instead of overwriting each other, via `intent(id)` handles
23
+ that claim, wait, and commit.
24
24
 
25
25
  ## In flight
26
26
 
@@ -0,0 +1,109 @@
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
+ location: 'Stockholm',
41
+ status: 'pending',
42
+ });
43
+ ```
44
+
45
+ The model key (`weatherReports`) becomes the client namespace
46
+ (`ablo.weatherReports`). The Zod fields become the create/update/read type
47
+ contract. You should not create a parallel string-keyed write path for the same
48
+ data.
49
+
50
+ ## Reads and writes
51
+
52
+ Use async reads when the row may not be local:
53
+
54
+ ```ts
55
+ const report = await ablo.weatherReports.retrieve(reportId);
56
+ const ready = await ablo.weatherReports.list({ where: { status: 'ready' } });
57
+ ```
58
+
59
+ Use synchronous local reads in render after data has synced:
60
+
61
+ ```ts
62
+ const report = ablo.weatherReports.get(reportId);
63
+ const pending = ablo.weatherReports.getAll({ where: { status: 'pending' } });
64
+ ```
65
+
66
+ Use model writes for every actor:
67
+
68
+ ```ts
69
+ await ablo.weatherReports.update(reportId, { status: 'ready' }, { wait: 'confirmed' });
70
+ ```
71
+
72
+ ## Coordination
73
+
74
+ Agents and background jobs often read, call a tool or model, then write later.
75
+ Wrap that slow span in `claim`:
76
+
77
+ ```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
+ });
82
+ ```
83
+
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
+
87
+ ## Storage boundary
88
+
89
+ Every schema model needs a backing store:
90
+
91
+ - Use Ablo-managed state when the row can live in Ablo.
92
+ - Use a Data Source when your app database remains canonical.
93
+
94
+ Do not pass a database URL to `Ablo(...)`. Trusted runtimes use `ABLO_API_KEY`.
95
+ Browser code goes through `<AbloProvider>` or a scoped session route, never a raw
96
+ API key.
97
+
98
+ ## Rules of thumb
99
+
100
+ - Start with fields and relations before load/index tuning.
101
+ - Import one schema into app code, server actions, agents, and Data Source routes.
102
+ - Keep direct database writes out of the coordinated path unless they are reported
103
+ back through Data Source events.
104
+ - Use `claim` for slow read -> think -> write spans.
105
+ - Use `readAt` + `onStale: 'reject'` when a write must fail if the row changed
106
+ after it was read.
107
+
108
+ For the shortest runnable path, start with [Quickstart](./quickstart.md). For a
109
+ production app, continue with [Integration Guide](./integration-guide.md).
@@ -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.
@@ -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';