@abloatai/ablo 0.48.0 → 0.49.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.
@@ -29,7 +29,7 @@ branch's database unavailable instead of silently writing somewhere else.
29
29
 
30
30
  Yes for the Ablo application path: model reads and lists, coordinated writes,
31
31
  claims, subscriptions, idempotency, confirmations, and transactional outbox
32
- settlement all work against localhost Postgres. The browser, server code, and
32
+ confirmation all work against localhost Postgres. The browser, server code, and
33
33
  agents still connect to Ablo Cloud; only database operations cross the narrow
34
34
  signed connector to your machine.
35
35
 
@@ -47,12 +47,43 @@ path with a network-reachable Postgres endpoint, PrivateLink/peering/VPN, or a
47
47
  database-capable secure tunnel. Do not expose Postgres without TLS,
48
48
  authentication, and network restrictions.
49
49
 
50
+ ### For localhost-first open-source projects
51
+
52
+ Do not require contributors to buy hosted Postgres or expose port 5432 merely to
53
+ run the project. Treat the connector as the default contributor topology:
54
+
55
+ ```json
56
+ {
57
+ "scripts": {
58
+ "ablo:setup": "ablo migrate",
59
+ "ablo:dev": "ablo dev --local"
60
+ }
61
+ }
62
+ ```
63
+
64
+ Keep `DATABASE_URL=postgres://…@localhost:5432/…` in `.env.example`, commit the
65
+ generated `ablo/data-source.ts` handler, and document two long-running processes:
66
+ the application and `npm run ablo:dev`. Contributors provide their own Ablo
67
+ branch credential through `ablo login`; the repository never contains it.
68
+
69
+ For collaborative models, route mutations through Ablo or the signed Data Source
70
+ adapter. If the existing project intentionally writes those same tables through
71
+ raw SQL or an unrelated ORM path, choose one explicitly:
72
+
73
+ - add the supported transactional outbox/source-push integration for those writes;
74
+ - state that only Ablo-mediated changes participate in live coordination locally; or
75
+ - make WAL integration tests opt-in through a secure direct tunnel or hosted test database.
76
+
77
+ That keeps the zero-cost localhost quickstart honest without weakening Ablo's
78
+ coordination boundary or pretending endpoint mode can see WAL.
79
+
50
80
  ### Local connector errors
51
81
 
52
82
  Every stable code links to the generated [error reference](https://docs.abloatai.com/errors):
53
83
 
54
84
  | Code | Meaning and fix |
55
85
  |---|---|
86
+ | `database_loopback_requires_connector` | A direct connection was configured with localhost. For the normal OSS/dev path, run `ablo migrate` and `ablo dev --local`; use a direct network route only when arbitrary SQL writes need WAL observation. |
56
87
  | `source_connector_not_attached` | The branch is connector-only but no process is attached. Start or restart `ablo dev --local`. |
57
88
  | `source_connector_unauthenticated` | The temporary branch key is missing, expired, or rejected. Rerun `ablo dev --local` to mint a fresh key. |
58
89
  | `source_connector_requires_secret_key` | The connector received the wrong key kind. Let `ablo dev` supply its branch-bound `sk_` key. |
@@ -2,11 +2,10 @@
2
2
 
3
3
  > Exactly what a confirmed write, a rejected stale write, and a held claim each promise.
4
4
 
5
- When an Ablo write succeeds, the server has accepted it and when two agents
6
- touch the same row, Ablo coordinates them instead of letting one silently
7
- overwrite the other. This page is the precise list of what you can count on:
8
- confirmed writes, stale-write protection, claims, and the audit trail behind
9
- every change.
5
+ When an Ablo write succeeds, the server has confirmed it. Concurrency behavior
6
+ depends on the write form you choose: plain writes are last-write-wins;
7
+ functional updates, stale guards, and claims add protection when a write depends
8
+ on earlier state. This page is the precise list of what each form promises.
10
9
 
11
10
  Claims don't lock. If another writer holds the row, `claim` waits for them,
12
11
  re-reads the fresh row, then hands it to you — so two writers serialize instead
@@ -49,18 +48,18 @@ Use `snapshot(...)` and `readAt` when a write depends on state the agent already
49
48
  read:
50
49
 
51
50
  ```ts
52
- const snap = ablo.snapshot({ weatherReports: 'report_stockholm' });
51
+ const report = await ablo.weatherReports.get({ id: 'report_stockholm' });
52
+ if (!report) throw new Error('report missing');
53
53
 
54
54
  await ablo.weatherReports.update({
55
- id: 'report_stockholm',
55
+ id: report.id,
56
56
  data: { status: 'ready' },
57
- readAt: snap.stamp,
58
- onStale: 'reject',
57
+ reads: [report],
59
58
  });
60
59
  ```
61
60
 
62
- `onStale: 'reject'` prevents lost updates. If the target changed after the
63
- snapshot, the server rejects the write instead of applying stale reasoning.
61
+ The returned row carries opaque evidence. If it changed after the read, the
62
+ server rejects the write instead of applying stale reasoning.
64
63
 
65
64
  Two other dispositions exist. `overwrite` applies the write with no stale check
66
65
  at all. `notify` **holds** the write, so the row is left as it stands, and hands
@@ -70,6 +69,11 @@ and re-issue; the rest of the batch still commits.
70
69
  See [Concurrency Convention](./concurrency-convention.md) for the full taxonomy,
71
70
  what each disposition is checked against, and where the convention stops.
72
71
 
72
+ A plain `update({ id, data })` carries no stale premise. If no one holds a claim
73
+ on the target, it is last-write-wins. That is appropriate for independent values
74
+ such as status flags, but not for a read-modify-write calculation. For the latter,
75
+ use the functional update form or pass exact returned rows through `reads`.
76
+
73
77
  ## Claim Coordination
74
78
 
75
79
  > The guarantee, not the how-to. Methods, the claim-state object, and the `claim.queue`
@@ -86,10 +90,12 @@ error out, when it should not return a row while someone else is mid-edit. Reads
86
90
  never block on a claim — to wait for a row to free up, `claim({ id })` it (the
87
91
  claim queues fairly behind the holder).
88
92
 
89
- A claim does not reject or block other writers; it announces work so peers
90
- serialize behind it rather than racing. While you hold a claim, the matching
91
- `ablo.<model>.update({ id, ... })` is rejected with `AbloStaleContextError` if the row
92
- changed underneath you after your claim point.
93
+ By default, a held claim rejects writes from other participants to the claimed
94
+ target. Contenders that call `claim` wait their turn; ordinary reads remain
95
+ open. An explicit model conflict policy can choose another disposition for a
96
+ participant kind. While you hold a claim, the matching
97
+ `ablo.<model>.update({ id, ... })` is rejected with `AbloStaleContextError` if
98
+ the row changed underneath you after your claim point.
93
99
 
94
100
  ## Agent Runs
95
101
 
@@ -172,12 +178,10 @@ separates workflow or deployment lanes sharing the same store.
172
178
 
173
179
  ## Storage Boundary
174
180
 
175
- Your rows live in your database; Ablo holds only the transaction log and the
176
- coordination state. Writes enter Ablo's commit chokepoint and land in your
177
- Postgres through a scoped writer role, and the WAL echo confirms them
178
- (`queued` `confirmed`). For a database that can't grant replication, Ablo
179
- forwards the write to a signed Data Source endpoint instead — the marked
180
- fallback. Either way Ablo never holds a database connection string. See
181
+ Your rows live in your database; Ablo holds change history and coordination
182
+ state. Writes land in your Postgres through a scoped writer role and are
183
+ confirmed from its authoritative change feed. For a database that cannot grant
184
+ replication, Ablo uses a signed Data Source endpoint instead. See
181
185
  [Connect Your Database](./data-sources.md).
182
186
 
183
187
  ## Writes
@@ -19,20 +19,20 @@ Ablo is a **coordination layer in front of your Postgres**. Agents, background
19
19
  jobs, and the people alongside them all change the same application data through
20
20
  one API, and Ablo makes sure their writes don't clobber each other.
21
21
 
22
- - **Writes go through Ablo:** `ablo.<model>.create / update / delete` enter Ablo's
23
- commit chokepoint where claims, ordering, and idempotency are enforced — and
24
- Ablo applies the change to your Postgres through a scoped writer role. The commit
25
- is accepted (`queued`) the moment Ablo takes it.
22
+ - **Writes go through Ablo:** `ablo.<model>.create / update / delete` are
23
+ authorized, made idempotent, and applied to your Postgres through a scoped
24
+ writer role. A plain write is last-write-wins. Use a functional update, a
25
+ explicit `reads: [returnedRow]`, or a claim when the new value depends on an
26
+ earlier read.
26
27
  - **Your database confirms the write.** Ablo tails your write-ahead log (WAL). When
27
28
  the row it wrote shows up there, the receipt is promoted to `confirmed`. So your
28
29
  database, not Ablo, is the source of truth for row state — the WAL echo is how
29
30
  Ablo *confirms*, not how it writes.
30
31
  - **Reads are live.** Ablo serves current state and keeps every connected client up
31
32
  to date off that same stream.
32
- - **Ablo stores only the log.** Ablo holds the ordered transaction log
33
- (`sync_deltas`) and the coordination state the claims, the ordering, the
34
- watermarks. Your rows live in your database. Ablo runs no DDL and owns no schema;
35
- your migration tool stays in charge of the shape of your database.
33
+ - **Ablo stores only the change history and coordination state.** Your rows live
34
+ in your database. Ablo does not run application migrations; your migration
35
+ tool stays in charge of tables and columns.
36
36
 
37
37
  That's the shape: **you write through Ablo → it lands in your Postgres → the WAL
38
38
  echo confirms it → everyone connected sees it live.**
@@ -45,21 +45,7 @@ echo confirms it → everyone connected sees it live.**
45
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. Awaiting a schema write waits for confirmation. |
49
-
50
- ### Why each primitive is separate
51
-
52
- Why are `Claim`, `Commit`, and `Receipt` separate things instead of one? Each
53
- does a job the others cannot. If you are coming from Replicache or Yjs you would
54
- expect just `Commit`. Here is what the other two buy you over that minimum:
55
-
56
- - **`Claim` is not a read lock.** Reads stay open. Claims serialize
57
- acting-on-the-row, so slow work can wait in FIFO order, re-read, and write
58
- from fresh state.
59
- - **`Receipt` is not a `200 OK`.** It is the durable artifact a commit produced:
60
- accepted commit id, server-assigned timestamps, stale-check outcome. It is
61
- addressable after the fact and replayable into a different client. A status
62
- code cannot be re-read by a sub-agent that was not on the original call.
48
+ | `Receipt` | Protocol | The result of a lower-level commit. Awaiting a schema write waits for confirmation. |
63
49
 
64
50
  ## Where your data lives
65
51
 
@@ -117,22 +103,6 @@ const latest = ablo.tasks.local.get('task_42'); // read after claiming, not from
117
103
  await ablo.tasks.update({ id: 'task_42', data: { status: 'done' } });
118
104
  ```
119
105
 
120
- ## Reading the architecture correctly
121
-
122
- The model changed once (ADR 0010), and older material described the earlier one.
123
- If you've read that a customer's app keeps writing while Ablo only tails the WAL,
124
- here is the current, correct reading:
125
-
126
- - **"Ablo tails the WAL."** True — to *confirm* your writes and serve live reads.
127
- It is not the write path. You write **through** Ablo.
128
- - **"Ablo is out of the write path."** Not anymore. Ablo is the write chokepoint;
129
- every write flows through its coordination and then into your database.
130
- - **"Ablo holds my rows."** Only on a development branch where you have not
131
- connected a database yet. Once you connect one, your rows live in *your*
132
- Postgres — Ablo keeps just the log.
133
- - **"Ablo migrates my schema."** No. Ablo writes rows through a scoped role and runs
134
- no DDL; your migrations own the shape of your database.
135
-
136
106
  For the setup details, see [Connect Your Database](./data-sources.md). For the
137
- coordination loop, see [Concurrency Convention](./concurrency-convention.md). For
107
+ coordination loop, see [Coordination](./coordination.md). For
138
108
  what `queued` and `confirmed` guarantee, see [Guarantees](./guarantees.md).
package/docs/identity.md CHANGED
@@ -14,6 +14,54 @@ NextAuth, WorkOS, your own session table. Ablo's job begins **after** you've
14
14
  authenticated the user: you hand Ablo the already-authenticated identity, and
15
15
  Ablo decides which **sync groups** that identity may read and write.
16
16
 
17
+ ## Inspect the credential the application is actually using
18
+
19
+ `ablo whoami` describes the developer running the CLI. Runtime code should
20
+ inspect `ablo.identity`, which is the server-confirmed `EffectiveAuthority` of
21
+ the credential attached to that client. It is never decoded or reconstructed
22
+ locally.
23
+
24
+ ```ts
25
+ import { Ablo } from '@abloatai/ablo';
26
+ import { CapabilityError } from '@abloatai/ablo';
27
+
28
+ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
29
+ await ablo.ready();
30
+
31
+ console.log(ablo.identity?.operations);
32
+ console.log(ablo.identity?.syncGroups);
33
+ ```
34
+
35
+ The HTTP and stateful clients expose the same value after `ready()`. A denied
36
+ scoped operation throws `CapabilityError`; compare its
37
+ `requiredCapability.scope` directly with `ablo.identity.operations`.
38
+
39
+ ```ts
40
+ try {
41
+ await ablo.tasks.update({ id, data: { status: 'done' } });
42
+ } catch (error) {
43
+ if (error instanceof CapabilityError) {
44
+ console.error('missing grant', error.requiredCapability);
45
+ }
46
+ }
47
+ ```
48
+
49
+ Do not broaden the credential in the client. A backend holding the project
50
+ secret mints a replacement, least-privilege agent credential with the schema-
51
+ typed grant:
52
+
53
+ ```ts
54
+ const session = await control.sessions.create({
55
+ agent: { id: agentId },
56
+ can: { tasks: ['read', 'update'] },
57
+ syncGroups: [syncGroup('workspace', workspaceId)],
58
+ });
59
+ ```
60
+
61
+ Install `session.token` in the agent process and call `ready()` again. The next
62
+ `ablo.identity` is the authority the server will enforce; no automatic grant
63
+ escalation occurs.
64
+
17
65
  So the integration question is never "how do I log into Ablo?" It's: *"My app
18
66
  already knows this request is user `U` in org `O`. How do I tell Ablo, so it
19
67
  scopes their realtime data correctly?"* The rest of this doc answers exactly
package/docs/index.md CHANGED
@@ -140,7 +140,7 @@ default caller, not a special one.
140
140
 
141
141
  - [How Ablo Works](./how-it-works.md) — the mental model in one page: you write through Ablo, it lands in your Postgres, the write-ahead log confirms it. **Read this first.**
142
142
  - [Coordination](./coordination.md) — `claim`, `claim.state`, and `claim.queue`: who holds a row, and who is waiting.
143
- - [Concurrency Convention](./concurrency-convention.md) — the governing rule for how concurrent writes resolve.
143
+ - [Concurrency Convention](./concurrency-convention.md) — the precise rule for guarded and unguarded writes.
144
144
  - [Guarantees](./guarantees.md) — what a confirmed write, a stale-write rejection, and a claim each promise.
145
145
  - [Idempotency](./idempotency.md) — make a retried write safe; what replays, what re-runs, and for how long.
146
146
  - [Schema Contract](./schema-contract.md) — one schema becomes typed clients, agent writes, React reads, and the push.
@@ -175,7 +175,7 @@ default caller, not a special one.
175
175
 
176
176
  - [API Reference](./api.md) — model-by-model method shape.
177
177
  - [Errors](./errors.md) — the code registry, its categories, and what to do about each.
178
- - [Version History & Migration](./migration.md) — every breaking change and its migration.
178
+ - [Upgrade Guide](./migration.md) — upgrade a pinned pre-1.0 SDK safely.
179
179
  - [Changelog](../CHANGELOG.md) — what shipped recently.
180
180
 
181
181
  ## Examples