@abloatai/ablo 0.46.0 → 0.48.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.
@@ -2,6 +2,71 @@
2
2
 
3
3
  > Keep the rows in your own Postgres while Ablo coordinates and confirms every write.
4
4
 
5
+ ## Localhost development
6
+
7
+ Ablo Cloud cannot dial `localhost`: from a cloud server, that name means the
8
+ cloud server itself, not your Mac or development container. A development child
9
+ branch can still use Postgres that listens only on your machine by running a
10
+ signed Data Source over Ablo's outbound reverse channel:
11
+
12
+ ```bash
13
+ npx ablo migrate # once: models + ablo_idempotency + ablo_outbox
14
+ npx ablo dev --local
15
+ ```
16
+
17
+ The command loads `ablo/data-source.ts`, registers the current child branch as
18
+ connector-only, and dials out to Ablo over an authenticated WebSocket. Postgres
19
+ continues listening only on your machine; `DATABASE_URL` never leaves the
20
+ process. This is protocol-scoped, not a general-purpose tunnel: only signed
21
+ Data Source load, list, commit, and event requests traverse it. Use
22
+ `--source <path>` when the handler lives elsewhere.
23
+
24
+ Keep `ablo dev --local` running alongside the application. It pushes schema
25
+ changes and owns the database connector; stopping it deliberately makes the
26
+ branch's database unavailable instead of silently writing somewhere else.
27
+
28
+ ### Is this full Ablo?
29
+
30
+ Yes for the Ablo application path: model reads and lists, coordinated writes,
31
+ claims, subscriptions, idempotency, confirmations, and transactional outbox
32
+ settlement all work against localhost Postgres. The browser, server code, and
33
+ agents still connect to Ablo Cloud; only database operations cross the narrow
34
+ signed connector to your machine.
35
+
36
+ It is not logical replication. Visibility depends on how a row is written:
37
+
38
+ | Write origin | Visible to Ablo in localhost mode? | Why |
39
+ |---|---:|---|
40
+ | `ablo.<model>.create/update/delete` | Yes | Ablo coordinates the write, the local adapter commits it with idempotency + outbox, and the outbox event confirms it. |
41
+ | Code using the signed Data Source adapter | Yes | The adapter records the row and authoritative event in one transaction. |
42
+ | A supported source push/outbox integration | Yes | It explicitly publishes the authoritative event to Ablo. |
43
+ | Raw SQL, `psql`, or an unrelated ORM write | No, not automatically | There is no WAL reader in signed-endpoint mode, and bypassing the adapter does not append `ablo_outbox`. |
44
+
45
+ If Ablo must observe every arbitrary SQL/ORM write, use the direct logical-WAL
46
+ path with a network-reachable Postgres endpoint, PrivateLink/peering/VPN, or a
47
+ database-capable secure tunnel. Do not expose Postgres without TLS,
48
+ authentication, and network restrictions.
49
+
50
+ ### Local connector errors
51
+
52
+ Every stable code links to the generated [error reference](https://docs.abloatai.com/errors):
53
+
54
+ | Code | Meaning and fix |
55
+ |---|---|
56
+ | `source_connector_not_attached` | The branch is connector-only but no process is attached. Start or restart `ablo dev --local`. |
57
+ | `source_connector_unauthenticated` | The temporary branch key is missing, expired, or rejected. Rerun `ablo dev --local` to mint a fresh key. |
58
+ | `source_connector_requires_secret_key` | The connector received the wrong key kind. Let `ablo dev` supply its branch-bound `sk_` key. |
59
+ | `source_connector_no_source_registered` | No endpoint source exists for this branch. Upgrade/rerun the CLI so registration happens before socket attachment. |
60
+ | `source_connector_localhost_required` | Connector-only registration used a non-local descriptor. Use `ablo dev --local`; deployed handlers use ordinary HTTPS endpoint registration. |
61
+ | `source_connector_timeout` | The handler or local Postgres exceeded the request deadline. Inspect the local process and database. |
62
+ | `source_connector_handler_error` | `ablo/data-source.ts` or its adapter threw. The local terminal contains the underlying error. |
63
+ | `source_connector_protocol_error` | CLI/SDK and service connector protocols disagree. Upgrade the CLI and SDK together. |
64
+ | `source_connector_production_not_enabled` | A root/production key attempted the development connector. Use a supported production route or explicitly enable production reverse-channel support. |
65
+
66
+ Disconnects, service restarts, and connector replacement are retryable. Keep the
67
+ same idempotency key: Ablo never falls back from this branch to hosted storage or
68
+ another database.
69
+
5
70
  You write through Ablo, and Ablo writes to your Postgres. A call to
6
71
  `ablo.<model>.create / update / delete` enters Ablo's commit chokepoint — where
7
72
  claims, ordering, and idempotency are enforced — and Ablo applies the change to
@@ -36,6 +101,36 @@ npx ablo connect apply --env-file .env.local --yes
36
101
  The explicit flag makes the credential choice visible and loads both the
37
102
  branch-bound key and database URL. Shell environment variables take precedence.
38
103
 
104
+ ### One database, several projects
105
+
106
+ Provider database URLs and Postgres schemas solve different isolation jobs:
107
+
108
+ ```text
109
+ database URL = production, staging, or preview environment
110
+ schema = application/project inside that database
111
+ ABLO_API_KEY = exact Ablo project branch to bind
112
+ ```
113
+
114
+ It is safe to keep several apps in one production database when each app has its
115
+ own schema:
116
+
117
+ ```bash
118
+ ABLO_API_KEY="$MAIL_KEY" DATABASE_URL="$PRODUCTION_URL" \
119
+ npx ablo connect apply --schema mail --yes
120
+
121
+ ABLO_API_KEY="$SLIDES_KEY" DATABASE_URL="$PRODUCTION_URL" \
122
+ npx ablo connect apply --schema slides --yes
123
+ ```
124
+
125
+ For a Neon or Supabase preview branch, use that branch's direct URL and keep the
126
+ schema name stable. Ablo binds one plane to `(database, schema)`: the same
127
+ database may add `billing`, but a second project cannot also claim `mail`.
128
+ Cross-organization conflicts reveal only that the binding is occupied.
129
+
130
+ Push the Ablo schema before connecting, or pass `--tables`. The publication is
131
+ an explicit list of schema-qualified mapped tables; Ablo never uses a
132
+ database-wide `FOR ALL TABLES` publication for this multi-project path.
133
+
39
134
  If scoped roles already exist but their passwords are unavailable, do not drop
40
135
  them or run `DROP OWNED`. Rotate them in place and re-register the fresh
41
136
  credentials:
@@ -60,11 +155,12 @@ without printing the secret. Retire the old variable after the move.
60
155
  ## Connect in one command
61
156
 
62
157
  ```bash
63
- npx ablo connect apply --url postgres://admin:...@host:5432/db
158
+ npx ablo connect apply --url postgres://admin:...@host:5432/db --schema mail
64
159
  ```
65
160
 
66
- Pass an admin connection string with `--url` and it creates the publication, the
67
- two scoped roles, and the grants, turns on logical decoding where it can, registers
161
+ Pass an admin connection string with `--url` and select the application namespace
162
+ with `--schema` (default `public`). It creates a per-binding publication, two
163
+ per-binding scoped roles, and the grants, turns on logical decoding where it can, registers
68
164
  both scoped roles with Ablo, and proves the setup by reconnecting and reading back.
69
165
  The admin credential is used on this machine only and never persisted — nothing is
70
166
  written to your `.env`, which keeps holding only `ABLO_API_KEY`. Pass `--show-sql`
@@ -78,7 +174,7 @@ to run it by hand or review exactly what changes.
78
174
 
79
175
  When Ablo creates the replication slot, it takes a consistent initial snapshot of
80
176
  every mapped table in the publication before following new changes. Rows that
81
- predate `ablo connect` therefore become available to `retrieve`, `list`, and
177
+ predate `ablo connect` therefore become available to `get`, `list`, and
82
178
  reactive `local.*` reads without an application backfill.
83
179
 
84
180
  Run `ablo connect check` before removing an existing HTTP/database read fallback.
@@ -87,6 +183,24 @@ write a script that updates every row to make it visible: an Ablo update require
87
183
  the row to be visible already, and touching application rows is neither necessary
88
184
  nor a safe bootstrap mechanism.
89
185
 
186
+ If a connection was snapshotted with an older replication role whose row-level
187
+ security hid historical rows, repair that role and request the load again without
188
+ deregistering or rotating credentials:
189
+
190
+ ```bash
191
+ npx ablo connect rotate # reasserts BYPASSRLS and safely re-registers both roles
192
+ npx ablo connect resnapshot # recreates only the slot; the load is asynchronous
193
+ npx ablo connect check # repeat until the existing-row load is complete
194
+ ```
195
+
196
+ Use the same `resnapshot` step after adding an existing populated table to the
197
+ publication. Following its future WAL changes is not enough to load rows written
198
+ before publication membership; the snapshot coverage guard therefore refuses to
199
+ record completion when even one mapped table is absent. Relation matching is
200
+ schema-qualified using the DataSource's configured `schema` (default `public`):
201
+ an identically named table in another Postgres schema neither counts as coverage
202
+ nor enters the snapshot or WAL stream for your model.
203
+
90
204
  ## The setup, step by step
91
205
 
92
206
  ### 1. Enable logical decoding
@@ -112,24 +226,34 @@ npx ablo connect
112
226
  `ablo connect` prints the exact, copy-pasteable setup SQL for **your** Postgres.
113
227
  Run it against your database as a superuser or the DB owner. It creates:
114
228
 
115
- - **A publication** naming the tables Ablo reads and confirms against
116
- (`ablo_publication`, the single canonical name the runtime subscribes to):
229
+ - **A per-binding publication** naming only the schema-qualified mapped tables
230
+ Ablo reads and confirms against. Its suffix is derived from the authenticated
231
+ Ablo plane and is stable across re-runs:
117
232
 
118
233
  ```sql
119
- CREATE PUBLICATION "ablo_publication" FOR ALL TABLES;
234
+ CREATE PUBLICATION "ablo_publication_<suffix>"
235
+ FOR TABLE "mail"."messages", "mail"."threads";
120
236
  ```
121
237
 
122
- Scope it to a subset with `npx ablo connect --tables a,b,c`.
238
+ Override the pushed model set with `--tables a,b,c`.
123
239
 
124
240
  - **A replication role:** it streams the WAL and `SELECT`s, nothing more. This is
125
241
  the role Ablo reads and confirms through. You choose the password; it never
126
242
  passes through Ablo's CLI or servers:
127
243
 
128
244
  ```sql
129
- CREATE ROLE "ablo_replicator" WITH REPLICATION LOGIN PASSWORD '<password>';
130
- GRANT SELECT ON ALL TABLES IN SCHEMA public TO "ablo_replicator";
245
+ CREATE ROLE "ablo_replicator_<suffix>" WITH
246
+ NOSUPERUSER BYPASSRLS NOCREATEDB NOCREATEROLE REPLICATION NOINHERIT
247
+ LOGIN PASSWORD '<password>';
248
+ GRANT SELECT ON TABLE "mail"."messages", "mail"."threads"
249
+ TO "ablo_replicator_<suffix>";
131
250
  ```
132
251
 
252
+ `BYPASSRLS` is required because the initial load is an ordinary `SELECT`,
253
+ while logical replication already exposes every row in the publication
254
+ independently of row-level-security policies. Keep this role's `SELECT`
255
+ grants scoped to the published tables; it has no write or DDL privileges.
256
+
133
257
  On Amazon RDS the `REPLICATION` attribute is granted, not set directly:
134
258
  `GRANT rds_replication TO "ablo_replicator";`.
135
259
 
@@ -139,15 +263,23 @@ Run it against your database as a superuser or the DB owner. It creates:
139
263
  can change rows in your tables; it cannot change your database:
140
264
 
141
265
  ```sql
142
- CREATE ROLE "ablo_writer" WITH LOGIN PASSWORD '<write-password>'
266
+ CREATE ROLE "ablo_writer_<suffix>" WITH LOGIN PASSWORD '<write-password>'
143
267
  NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE NOREPLICATION NOINHERIT;
144
- GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "ablo_writer";
268
+ GRANT SELECT, INSERT, UPDATE, DELETE
269
+ ON TABLE "mail"."messages", "mail"."threads"
270
+ TO "ablo_writer_<suffix>";
145
271
  ```
146
272
 
147
273
  Rename either role with `--role <name>` / `--write-role <name>`.
148
274
 
149
- The **replication slot** is created and owned by Ablo's runtime when it first
150
- subscribes you don't pre-create it.
275
+ The schema-local `ablo_idempotency` ledger lives beside that app's tables. The
276
+ **replication slot** (`ablo_slot_<suffix>`) is created and owned by Ablo's runtime
277
+ when it first subscribes — you don't pre-create it. Registration checks
278
+ `max_replication_slots` first and explains how to free or add capacity.
279
+
280
+ `ablo connect --manual` retains the legacy canonical object names for
281
+ compatibility and is therefore single-binding within a physical database. Use
282
+ `connect apply --schema …` when several projects share that database.
151
283
 
152
284
  ### 3. Register the database with Ablo
153
285
 
@@ -197,6 +329,8 @@ Your **app** holds only the API key — never a connection string:
197
329
  ```bash
198
330
  # .env — server runtime only, never the browser
199
331
  ABLO_API_KEY=sk_...
332
+ ABLO_PROJECT_ID=proj_...
333
+ ABLO_BRANCH_ID=br_...
200
334
  ```
201
335
 
202
336
  ```ts
@@ -206,9 +340,18 @@ import { schema } from './ablo/schema';
206
340
  export const ablo = Ablo({
207
341
  schema,
208
342
  apiKey: process.env.ABLO_API_KEY,
343
+ projectId: process.env.ABLO_PROJECT_ID,
344
+ branchId: process.env.ABLO_BRANCH_ID,
209
345
  });
210
346
  ```
211
347
 
348
+ `ABLO_PROJECT_ID` and `ABLO_BRANCH_ID` are safety assertions, not routing inputs.
349
+ The API key still selects the project and branch; during `ready()` Ablo asks the
350
+ server what the key actually targets and refuses startup when either coordinate
351
+ differs. `ablo dev` writes all three values together, so accidentally exporting
352
+ a slides key into the mail app—or a mail development key into production—fails
353
+ before any read, write, or subscription begins.
354
+
212
355
  The Ablo schema describes **only your synced, collaborative models** — the rows
213
356
  Ablo coordinates and fans out in realtime. It is _not_ your whole-database schema
214
357
  and does _not_ replace your `schema.prisma` (or Drizzle schema). Your auth,
@@ -227,7 +370,7 @@ landed:
227
370
  await ablo.weatherReports.update({ id: 'report_stockholm', data: { high: 21 } });
228
371
 
229
372
  // Block until your database has it and the WAL echo confirms.
230
- await ablo.weatherReports.update({ id: 'report_stockholm', data: { high: 21 }, wait: 'confirmed' });
373
+ await ablo.weatherReports.update({ id: 'report_stockholm', data: { high: 21 } });
231
374
 
232
375
  // Reads are live off the same stream.
233
376
  const report = ablo.weatherReports.local.get('report_stockholm');
package/docs/debugging.md CHANGED
@@ -267,7 +267,6 @@ import { AbloError } from '@abloatai/ablo';
267
267
  try {
268
268
  await ablo.documents.create({
269
269
  data,
270
- wait: 'confirmed',
271
270
  });
272
271
  } catch (error) {
273
272
  if (error instanceof AbloError) {
@@ -276,10 +275,10 @@ try {
276
275
  }
277
276
  ```
278
277
 
279
- With `wait: 'confirmed'`, the awaited call rejects with that complete typed
280
- error. `onMutationFailure` remains the notification channel for optimistic
281
- writes that return before the server answers; it is not required to recover
282
- details from a confirmed write.
278
+ An awaited model write rejects with that complete typed error.
279
+ `onMutationFailure` remains the notification channel for deliberately
280
+ unawaited optimistic writes; it is not required to recover details from an
281
+ awaited write.
283
282
 
284
283
  ### Local reads versus a confirmed server read
285
284
 
@@ -75,7 +75,6 @@ export async function markDone(taskId: string) {
75
75
  // ablo.tasks.update({
76
76
  // id: claim.data.id,
77
77
  // data: { status: 'done' },
78
- // wait: 'confirmed',
79
78
  // readAt: <claim snapshot version>,
80
79
  // onStale: 'reject',
81
80
  // });
@@ -86,7 +85,6 @@ export async function markDone(taskId: string) {
86
85
  const updated = await ablo.tasks.update({
87
86
  id: claim.data.id,
88
87
  data: { status: 'done' },
89
- wait: 'confirmed',
90
88
  });
91
89
 
92
90
  return { status: 'done', task: updated };
@@ -255,7 +255,6 @@ await ablo.weatherReports.update({
255
255
  data: { status: 'ready' },
256
256
  readAt: snap.stamp,
257
257
  onStale: 'reject',
258
- wait: 'confirmed',
259
258
  });
260
259
  ```
261
260
 
@@ -294,7 +293,6 @@ await ablo.weatherReports.update({
294
293
  data: { status: 'ready' },
295
294
  readAt: snap.stamp,
296
295
  onStale: 'reject',
297
- wait: 'confirmed',
298
296
  });
299
297
  ```
300
298
 
@@ -23,7 +23,7 @@ app/
23
23
  route.ts # mints a per-user ek_ token for the browser
24
24
  tasks/
25
25
  [id]/
26
- page.tsx # RSC: retrieve + render
26
+ page.tsx # RSC: get + render
27
27
  actions.ts # Server Action: claim, then write
28
28
  TaskEditor.tsx # Client: live updates
29
29
  lib/
@@ -168,7 +168,6 @@ export async function markDone(id: string) {
168
168
  id,
169
169
  data: { status: 'done' },
170
170
  claim,
171
- wait: 'confirmed',
172
171
  });
173
172
 
174
173
  return { status: 'done', task };
@@ -69,7 +69,6 @@ export async function completeTask(taskId: string, workerId: string) {
69
69
  const updated = await ablo.tasks.update({
70
70
  id: claim.data.id,
71
71
  data: { status: 'done' },
72
- wait: 'confirmed',
73
72
  });
74
73
 
75
74
  return { status: 'done', task: updated };
@@ -79,8 +78,8 @@ export async function completeTask(taskId: string, workerId: string) {
79
78
 
80
79
  `get({ id })` is an async server read — it hits the server and returns the
81
80
  row (or `undefined`, which the early `not_found` guard handles). The update runs
82
- while the claim is held, and `wait: 'confirmed'` makes it resolve only once your
83
- database has confirmed the row landed.
81
+ while the claim is held; awaiting it resolves only once your database has
82
+ confirmed the row landed.
84
83
 
85
84
  The two options on the claim:
86
85
 
package/docs/groups.md CHANGED
@@ -266,7 +266,7 @@ try {
266
266
  await ablo.tasks.update({ id, data });
267
267
  } catch (err) {
268
268
  if (err.code === 'stale_context') {
269
- const fresh = await ablo.documents.retrieve({ id: 's-1' }); // read
269
+ const fresh = await ablo.documents.get({ id: 's-1' }); // read
270
270
  await ablo.documents.track({ id: 's-1', onStale: 'reject' }); // acknowledge
271
271
  await ablo.tasks.update({ id, data: reconsider(fresh) }); // now it lands
272
272
  }
@@ -14,14 +14,13 @@ of clobbering.
14
14
 
15
15
  ## Confirmed Writes
16
16
 
17
- `wait: 'confirmed'` resolves only after the server accepts the write and returns
18
- the authoritative sync cursor.
17
+ Awaiting a schema model write resolves only after authoritative confirmation
18
+ and returns the updated row.
19
19
 
20
20
  ```ts
21
21
  const updated = await ablo.weatherReports.update({
22
22
  id: 'report_stockholm',
23
23
  data: { status: 'ready' },
24
- wait: 'confirmed',
25
24
  });
26
25
  ```
27
26
 
@@ -37,9 +36,8 @@ Schema model writes return the updated model row.
37
36
  Schema model writes update local state optimistically. This keeps UI and agent
38
37
  tools responsive while the commit is sent to the server.
39
38
 
40
- - With `wait: 'queued'` or omitted, the promise resolves after the local mutation
41
- is queued.
42
- - With `wait: 'confirmed'`, the promise waits for server confirmation.
39
+ - The local model changes immediately, before the promise settles.
40
+ - The promise always waits for authoritative confirmation.
43
41
  - If the server rejects the write, the SDK rolls back the optimistic change and
44
42
  raises a typed error.
45
43
 
@@ -58,7 +56,6 @@ await ablo.weatherReports.update({
58
56
  data: { status: 'ready' },
59
57
  readAt: snap.stamp,
60
58
  onStale: 'reject',
61
- wait: 'confirmed',
62
59
  });
63
60
  ```
64
61
 
@@ -7,7 +7,7 @@ whole model — everything below explains what it means and how to use it.
7
7
 
8
8
  ```ts
9
9
  // You call Ablo. Ablo lands the change in your database and confirms it.
10
- await ablo.tasks.update({ id: 'task_42', data: { status: 'done' }, wait: 'confirmed' });
10
+ await ablo.tasks.update({ id: 'task_42', data: { status: 'done' } });
11
11
 
12
12
  // Reads come back live, kept current from your database.
13
13
  const task = ablo.tasks.local.get('task_42');
@@ -42,10 +42,10 @@ echo confirms it → everyone connected sees it live.**
42
42
  | Primitive | Plane | Purpose |
43
43
  |---|---|---|
44
44
  | `Schema` | State | Declares typed models the app and agents can read and write. |
45
- | `Model` | State | The generated `ablo.<model>` model. Use `retrieve`/`list` (async reads), `local.get`/`local.list`/`local.count` (the same verbs, synchronous and local-only), `create`, `update`, and `delete`. |
45
+ | `Model` | State | The generated `ablo.<model>` model. Use `get`/`list` (async reads), `local.get`/`local.list`/`local.count` (the same verbs, synchronous and local-only), `create`, `update`, and `delete`. |
46
46
  | `Claim` | Coordination | Who is working on a target. Taken via `ablo.<model>.claim({ id })` and read via `ablo.<model>.claim.state({ id })`. Ephemeral, never persisted. |
47
47
  | `Commit` | Protocol | The durable write underneath model updates. Most users do not call it directly. |
48
- | `Receipt` | Protocol | The lower-level durable result for custom runtimes. Schema writes use `wait: 'confirmed'`. |
48
+ | `Receipt` | Protocol | The lower-level durable result for custom runtimes. Awaiting a schema write waits for confirmation. |
49
49
 
50
50
  ### Why each primitive is separate
51
51
 
@@ -102,9 +102,9 @@ import Ablo from '@abloatai/ablo';
102
102
  import { schema } from './ablo/schema';
103
103
  export const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
104
104
 
105
- // 5. Write through Ablo. It lands in your Postgres; wait: 'confirmed' blocks
106
- // until the WAL echo proves the row is there.
107
- await ablo.tasks.update({ id: 'task_42', data: { status: 'done' }, wait: 'confirmed' });
105
+ // 5. Write through Ablo. Local state changes immediately; await blocks until
106
+ // the authoritative feed proves the row is there.
107
+ await ablo.tasks.update({ id: 'task_42', data: { status: 'done' } });
108
108
 
109
109
  // 6. Read — live, no fetch loop.
110
110
  const task = ablo.tasks.local.get('task_42');
@@ -14,7 +14,6 @@ await ablo.tasks.update({
14
14
  id: taskId,
15
15
  data: { status: 'done' },
16
16
  idempotencyKey: `task:${taskId}:mark-done:v1`,
17
- wait: 'confirmed',
18
17
  });
19
18
  ```
20
19
 
package/docs/identity.md CHANGED
@@ -517,7 +517,7 @@ an agent pointed at the entities it's working on. You **never hand-write**
517
517
  const ablo = Ablo({ schema, apiKey: session.token });
518
518
  ```
519
519
 
520
- 2. **Automatically, on read or claim.** Reading a row (`retrieve`/`get`/
520
+ 2. **Automatically, on read or claim.** Reading a row (`get`/
521
521
  `claim.state`) auto-enrolls the client in that row's entity group
522
522
  (**read-interest**), and `claim`-ing it pins a **write-intent** subscription.
523
523
  So an agent's reachable set **accretes** as it works — no extra subscribe call.
package/docs/index.md CHANGED
@@ -14,7 +14,6 @@ await using claim = await ablo.reports.claim({ id: reportId });
14
14
  await ablo.reports.update({
15
15
  id: claim.data.id,
16
16
  data: { forecast: await generateForecast(claim.data) },
17
- wait: 'confirmed',
18
17
  });
19
18
  ```
20
19
 
@@ -90,7 +89,7 @@ based on a row that has since changed is turned away rather than applied.
90
89
  const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY, transport: 'http' });
91
90
  ```
92
91
 
93
- Read with `list` / `retrieve`, coordinate with `claim`, write with `create` / `update` /
92
+ Read with `list` / `get`, coordinate with `claim`, write with `create` / `update` /
94
93
  `delete`. See [Agents](./agents.md) for the loop and [API Reference](./api.md) for the shape.
95
94
  </Step>
96
95
 
@@ -77,8 +77,7 @@ When handing this to a coding agent, give it a concrete target:
77
77
  ```txt
78
78
  Add Ablo to this app for one model your agents edit.
79
79
  Run npx ablo dev and use its branch-bound key. Declare schema, add the Ablo client, replace
80
- one write with ablo.<model>.update(..., { readAt, onStale: 'reject',
81
- wait: 'confirmed' }), and add a smoke test for two concurrent writers.
80
+ one write with ablo.<model>.update(..., { readAt, onStale: 'reject' }), and add a smoke test for two concurrent writers.
82
81
  ```
83
82
 
84
83
  ## 1. Declare A Schema
@@ -261,9 +260,9 @@ Reads come in two flavors, and you pick based on whether you can wait.
261
260
  store) — they're async, so you `await` them. `local.get(id)`,
262
261
  `local.list({ where })`, and `local.count({ where })` read the already-synced local
263
262
  graph synchronously, so they're the ones you call in render — and the ones you
264
- use inside a `useAblo` selector, never the async `retrieve`/`list`.
263
+ use inside a `useAblo` selector, never the async `get`/`list`.
265
264
 
266
- Use `retrieve` when the row may not be local yet — it fetches from the server
265
+ Use `get` when the row may not be local yet — it fetches from the server
267
266
  and waits.
268
267
 
269
268
  ```ts
@@ -316,7 +315,7 @@ const ablo = useAblo();
316
315
  For simple writes:
317
316
 
318
317
  ```ts
319
- await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' }, wait: 'confirmed' });
318
+ await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' } });
320
319
  ```
321
320
 
322
321
  For writes based on state the user or agent already read, snapshot first and
@@ -330,12 +329,12 @@ await ablo.weatherReports.update({
330
329
  data: { status: 'ready' },
331
330
  readAt: snap.stamp,
332
331
  onStale: 'reject',
333
- wait: 'confirmed',
334
332
  });
335
333
  ```
336
334
 
337
- `wait: 'confirmed'` resolves after the server accepts the write. Rejections roll
338
- back optimistic local state and throw a typed `AbloError`.
335
+ The local row changes optimistically at once. Awaiting the model write waits for
336
+ authoritative confirmation; a rejection rolls the optimistic state back and
337
+ throws a typed `AbloError`.
339
338
 
340
339
  ## 5. Multiplayer Is Automatic
341
340
 
@@ -468,7 +467,6 @@ if (!claimed) return;
468
467
  await ablo.weatherReports.update({
469
468
  id: claimed.id,
470
469
  data: { status: 'ready', forecast: await getForecast(claimed) },
471
- wait: 'confirmed',
472
470
  });
473
471
  ```
474
472
 
@@ -489,7 +487,6 @@ const completeReport = tool({
489
487
  data: { status: 'ready', forecast },
490
488
  readAt: snap.stamp,
491
489
  onStale: 'reject',
492
- wait: 'confirmed',
493
490
  });
494
491
  },
495
492
  });
@@ -58,7 +58,6 @@ export const approveOrder = inngest.createFunction(
58
58
  id: orderId,
59
59
  data: { status: 'approved', approvalNote },
60
60
  idempotencyKey: `${operationId}:approve-order:${orderId}`,
61
- wait: 'confirmed',
62
61
  }),
63
62
  );
64
63
  },
@@ -99,7 +98,7 @@ For every mutating step:
99
98
  1. Accept a stable business operation ID in the triggering event.
100
99
  2. Derive a separate key for each logical effect.
101
100
  3. Reuse the key and mutation body for every retry.
102
- 4. Use `wait: 'confirmed'` when later steps depend on the authoritative result.
101
+ 4. Await every model write before a later step depends on its authoritative result.
103
102
  5. Treat reuse of a key with another body as an application bug.
104
103
  6. Keep the complete retry horizon within Ablo's documented
105
104
  [idempotency retention window](../idempotency.md).
@@ -133,7 +132,6 @@ await step.run('apply-order-review', () =>
133
132
  id: orderId,
134
133
  data: { status: 'approved', approvalNote },
135
134
  idempotencyKey: `${operationId}:apply-order-review:${orderId}`,
136
- wait: 'confirmed',
137
135
  }),
138
136
  );
139
137
  ```
@@ -167,7 +165,6 @@ await step.run('apply-review-with-claim', async () => {
167
165
  data: { status: 'approved', approvalNote },
168
166
  claim,
169
167
  idempotencyKey: `${operationId}:apply-review:${orderId}`,
170
- wait: 'confirmed',
171
168
  });
172
169
  } finally {
173
170
  await claim.release();
@@ -46,7 +46,6 @@ export async function approveOrder(input: ApproveOrderInput) {
46
46
  approvalNote: input.approvalNote,
47
47
  },
48
48
  idempotencyKey: input.idempotencyKey,
49
- wait: 'confirmed',
50
49
  });
51
50
  }
52
51
  ```
@@ -135,7 +134,7 @@ For every mutating Activity:
135
134
  1. Create the key in deterministic Workflow code or accept a stable operation
136
135
  ID in the Workflow input.
137
136
  2. Reuse the key and mutation body for every retry of the same logical effect.
138
- 3. Use `wait: 'confirmed'` when later Workflow steps depend on the
137
+ 3. Await each model write before later Workflow steps depend on the
139
138
  authoritative database result.
140
139
  4. Treat reuse of a key with a different body as an application bug.
141
140
  5. Keep the Temporal retry horizon within Ablo's documented
@@ -165,7 +164,6 @@ export async function applyReview(orderId: string, note: string) {
165
164
  id: claim.data.id,
166
165
  data: { approvalNote: note },
167
166
  claim,
168
- wait: 'confirmed',
169
167
  });
170
168
  }
171
169
  ```
@@ -69,7 +69,7 @@ FNV-1a content hash used for connect-time gating. Round-trip tested in
69
69
  error, createdBy, createdAt, activatedAt)`, unique `(orgId, version)`. State
70
70
  `pending|validated|active|overwritten|failed`, ≤1 active per tenant (Convex
71
71
  `_schemas` machine; Zero's "row in the operational DB"). *Migration written,
72
- not applied — 0 users, Neon direct-endpoint rule.*
72
+ not applied — 0 users at the time.*
73
73
  - **`pgSchemaStore` / `memorySchemaStore`** (`schemaStore.ts`) — mirrors
74
74
  `pgApiKeyStore`. `insertPending` assigns `MAX(version)+1`; `activate` is a
75
75
  transaction that demotes the current active → `overwritten` then promotes the
@@ -1,5 +1,9 @@
1
1
  # Repository Structure
2
2
 
3
+ For a verb-by-verb guide to declarations and implementations—including exactly
4
+ where `create`, `update`, `delete`, and `claim` live—read the public
5
+ [`CODEMAP.md`](../../CODEMAP.md).
6
+
3
7
  The public repository preserves the same ownership boundaries as the main
4
8
  monorepo. `@abloatai/ablo` is the product package; the packages beneath it are
5
9
  implementation owners and first-party extension surfaces.
package/docs/migration.md CHANGED
@@ -293,7 +293,7 @@ helper, and the agent/task type family (`Agent`, `AgentOptions`,
293
293
  + const { token } = await server.sessions.create({ agent: { id: agentId } });
294
294
  + const agent = Ablo({ schema, apiKey: token });
295
295
  + await using claim = await agent.tasks.claim({ id });
296
- + await agent.tasks.update({ id, data: { status: 'done' }, wait: 'confirmed' });
296
+ + await agent.tasks.update({ id, data: { status: 'done' } });
297
297
  ```
298
298
 
299
299
  Per-run token/cost now lives in Langfuse, not an `agent_tasks` table. The only
@@ -324,10 +324,10 @@ modifier are named siblings. Reactive local reads stay on the synchronous
324
324
 
325
325
  ```diff
326
326
  - await ablo.tasks.update(id, { status: 'done' }, { wait: 'confirmed' })
327
- + await ablo.tasks.update({ id, data: { status: 'done' }, wait: 'confirmed' })
327
+ + await ablo.tasks.update({ id, data: { status: 'done' } })
328
328
 
329
329
  - await ablo.tasks.retrieve(id)
330
- + await ablo.tasks.retrieve({ id })
330
+ + await ablo.tasks.get({ id })
331
331
 
332
332
  - useAblo((ablo) => ablo.tasks.retrieve(id)) ?? serverTask
333
333
  + useAblo((ablo) => ablo.tasks.local.get(id)) ?? serverTask