@abloatai/ablo 0.30.2 → 0.32.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.
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +53 -12
- package/README.md +12 -14
- package/dist/cli.cjs +1787 -1329
- package/dist/coordination/schema.d.ts +4 -2
- package/dist/coordination/schema.js +23 -9
- package/dist/errorCodes.d.ts +1 -0
- package/dist/errorCodes.js +2 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +1 -1
- package/dist/schema/schema.d.ts +57 -0
- package/dist/schema/schema.js +53 -0
- package/dist/schema/select.js +1 -0
- package/dist/schema/serialize.d.ts +3 -1
- package/dist/schema/serialize.js +7 -1
- package/dist/source/adapters/drizzle.js +4 -3
- package/dist/source/adapters/kysely.js +4 -4
- package/dist/source/adapters/prisma.js +3 -3
- package/dist/source/idempotency.d.ts +19 -4
- package/dist/source/idempotency.js +19 -4
- package/dist/source/migrations.d.ts +4 -1
- package/dist/source/migrations.js +36 -5
- package/docs/agent-messaging.md +1 -2
- package/docs/api.md +4 -4
- package/docs/audit.md +35 -23
- package/docs/client-behavior.md +7 -4
- package/docs/concurrency-convention.md +3 -3
- package/docs/data-sources.md +141 -102
- package/docs/examples/agent-human.md +2 -2
- package/docs/examples/ai-sdk-tool.md +1 -1
- package/docs/examples/nextjs.md +1 -1
- package/docs/examples/server-agent.md +2 -2
- package/docs/groups.md +160 -0
- package/docs/guarantees.md +9 -7
- package/docs/how-it-works.md +110 -0
- package/docs/integration-guide.md +8 -7
- package/docs/interaction-model.md +3 -3
- package/docs/quickstart.md +38 -31
- package/docs/schema-contract.md +7 -5
- package/docs/webhooks.md +23 -22
- package/llms.txt +10 -4
- package/package.json +1 -1
package/docs/audit.md
CHANGED
|
@@ -2,33 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
The audit log records who changed what in your org, and when — including
|
|
4
4
|
changes an AI agent made on a person's behalf. Every change is one row, and the
|
|
5
|
-
rows are
|
|
6
|
-
You can filter it, page through it, and export it.
|
|
7
|
-
|
|
8
|
-
Every commit becomes one row.
|
|
5
|
+
rows are chained with a keyed hash (HMAC-SHA256) so you can later prove the
|
|
6
|
+
history wasn't altered. You can filter it, page through it, and export it.
|
|
9
7
|
|
|
10
8
|
## Row shape
|
|
11
9
|
|
|
10
|
+
Each stored row carries both the attribution fields — who acted, on whose behalf,
|
|
11
|
+
with which key — and the chain columns that make the log tamper-evident:
|
|
12
|
+
|
|
12
13
|
```ts
|
|
13
14
|
{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
15
|
+
id: string,
|
|
16
|
+
occurredAt: '2026-05-14T14:22:01.034Z',
|
|
17
|
+
actorKind: 'user' | 'agent' | 'system',
|
|
18
|
+
actorId: string,
|
|
19
|
+
onBehalfOfKind: 'user' | 'agent' | 'system' | null,
|
|
20
|
+
onBehalfOfId: string | null,
|
|
21
|
+
capabilityId: string | null, // the API key/capability used for the write
|
|
22
|
+
capabilityLabel: string | null, // its human-readable name, for scanning the log
|
|
23
|
+
delegationChainRootUserId: string | null, // always points at a human
|
|
24
|
+
actionType: string, // e.g. 'weatherReport.update'
|
|
25
|
+
modelName: string, // e.g. 'claude-opus-4-8'
|
|
26
|
+
confirmationState: 'auto' | 'previewed' | 'approved' | 'required_human_approval' | 'auto_historical',
|
|
27
|
+
diffSummary: unknown,
|
|
28
|
+
// chain columns — carried on every stored row, checked by verify (below)
|
|
29
|
+
chainSeq: number,
|
|
30
|
+
prevHash: string,
|
|
31
|
+
rowHash: string,
|
|
29
32
|
}
|
|
30
33
|
```
|
|
31
34
|
|
|
35
|
+
`confirmationState` records whether an agent's write ran on its own (`auto`), was
|
|
36
|
+
shown first (`previewed`), was signed off (`approved`), or is still waiting on a
|
|
37
|
+
person (`required_human_approval`) — it's also a filter on the list endpoint.
|
|
38
|
+
|
|
32
39
|
## Delegation chain
|
|
33
40
|
|
|
34
41
|
Every action traces back to a human. Even when an agent makes the change,
|
|
@@ -53,9 +60,14 @@ or, on tamper:
|
|
|
53
60
|
|
|
54
61
|
```json
|
|
55
62
|
{ "ok": false, "brokenAtSeq": 8419, "reason": "hash_mismatch",
|
|
56
|
-
"expectedHash": "
|
|
63
|
+
"expectedHash": "a3f1c9…", "foundHash": "b7e04d…" }
|
|
57
64
|
```
|
|
58
65
|
|
|
66
|
+
Hashes come back as plain hex. `expectedHash`/`foundHash` accompany a
|
|
67
|
+
`hash_mismatch` or `prev_hash_mismatch`; the other reasons (`sequence_gap`,
|
|
68
|
+
`missing_root`, `no_rows`) stand on their own. Recomputing a row's hash needs the
|
|
69
|
+
org's HMAC key, so verification runs where that secret is available.
|
|
70
|
+
|
|
59
71
|
## Filter and paginate
|
|
60
72
|
|
|
61
73
|
The dashboard at `/[orgSlug]/audit` is the UI for this. The same filters
|
|
@@ -82,6 +94,6 @@ JSON `GET` endpoint above using `nextCursor`.
|
|
|
82
94
|
## Compliance posture
|
|
83
95
|
|
|
84
96
|
The [audit log landing page](/audit-log) is the marketing-side description.
|
|
85
|
-
The
|
|
86
|
-
`@ablo/audit-chain` package — embeddable if you
|
|
87
|
-
detached service.
|
|
97
|
+
The HMAC-SHA256 chain algorithm and its semantics live in the
|
|
98
|
+
`@ablo/audit-chain` package — the reference implementation, embeddable if you
|
|
99
|
+
need to verify chains in a detached service (given the org's HMAC key).
|
package/docs/client-behavior.md
CHANGED
|
@@ -39,8 +39,9 @@ Common options:
|
|
|
39
39
|
| `dangerouslyAllowBrowser` | Required before sending an API key from browser code. Prefer a server route instead. |
|
|
40
40
|
|
|
41
41
|
Your database connects out of band — through logical replication (`npx ablo
|
|
42
|
-
connect`) or
|
|
43
|
-
|
|
42
|
+
connect`), or the signed [Data Source](./data-sources.md) endpoint as the
|
|
43
|
+
fallback for databases that can't grant replication — so the client holds only
|
|
44
|
+
`apiKey`, never a connection string. See
|
|
44
45
|
[Connect Your Database](./data-sources.md) for the full setup.
|
|
45
46
|
|
|
46
47
|
## Model Methods
|
|
@@ -95,8 +96,10 @@ such as `useAblo((ablo) => ablo.weatherReports.claim.state({ id }))`
|
|
|
95
96
|
receive active claim state. There is
|
|
96
97
|
no extra multiplayer setup beyond routing shared state through Ablo.
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
Writes flow through Ablo's commit chokepoint and land in your database, so every
|
|
100
|
+
actor routing through Ablo is coordinated. The one write it can't coordinate is
|
|
101
|
+
one made directly against your database, around Ablo — the WAL echo still catches
|
|
102
|
+
it for reads, but it bypasses claims and ordering.
|
|
100
103
|
|
|
101
104
|
## Per-Write Options
|
|
102
105
|
|
|
@@ -161,7 +161,7 @@ and `currentValues` is empty (re-read the group).
|
|
|
161
161
|
|
|
162
162
|
What the convention **guarantees**, and where it **stops**:
|
|
163
163
|
|
|
164
|
-
1. **Engine surfaces, actor decides.**
|
|
164
|
+
1. **Engine surfaces, actor decides.** Under `notify` the engine never
|
|
165
165
|
repairs, merges, or re-plans. It reports `currentValues` and the actor (agent
|
|
166
166
|
or human) owns the resolution. The engine does not distinguish them — it is
|
|
167
167
|
actor-neutral by design.
|
|
@@ -179,7 +179,7 @@ What the convention **guarantees**, and where it **stops**:
|
|
|
179
179
|
shared database, which are inherently reversible (prior value in
|
|
180
180
|
`sync_deltas`). **Irreversible external side-effects** (emails, payments,
|
|
181
181
|
third-party calls) are *out of scope* — the engine cannot hold or undo them,
|
|
182
|
-
so they must not be gated by `
|
|
182
|
+
so they must not be gated by `notify`.
|
|
183
183
|
|
|
184
184
|
5. **Defaults.** A plain write (no `readAt`) is last-writer-wins with **no**
|
|
185
185
|
check. A guarded write with `readAt` but no `onStale` defaults to `reject`
|
|
@@ -202,7 +202,7 @@ What the convention **guarantees**, and where it **stops**:
|
|
|
202
202
|
These are deliberately left open; they change behavior and are the user's call.
|
|
203
203
|
|
|
204
204
|
- **Default disposition for agents.** Should an agent-participant guarded write
|
|
205
|
-
default to `
|
|
205
|
+
default to `notify` (philosophy-aligned: surface, don't overwrite) instead of
|
|
206
206
|
`reject` (back-compat)? Trade-off: alignment vs. a behavior change for existing
|
|
207
207
|
agent callers.
|
|
208
208
|
- **Read-deps through the policy seam.** Should read-set conflicts also pass
|
package/docs/data-sources.md
CHANGED
|
@@ -1,31 +1,49 @@
|
|
|
1
1
|
# Connect Your Database
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
You write through Ablo, and Ablo writes to your Postgres. A call to
|
|
4
|
+
`ablo.<model>.create / update / delete` enters Ablo's commit chokepoint — where
|
|
5
|
+
claims, ordering, and idempotency are enforced — and Ablo applies the change to
|
|
6
|
+
your database through a scoped role. Your rows live in your database, which stays
|
|
7
|
+
the system of record. Ablo reads your write-ahead log (WAL) to confirm each write
|
|
8
|
+
landed and to keep every connected human and agent current.
|
|
9
|
+
|
|
10
|
+
Ablo writes your **rows**; it never touches your **schema**. It runs no DDL and no
|
|
11
|
+
migrations — your migration tool stays in charge of the shape of your database.
|
|
12
|
+
Ablo only writes rows into tables you already have, through a role scoped to
|
|
13
|
+
exactly that.
|
|
14
|
+
|
|
15
|
+
> **Just trying Ablo?** You don't need a database to start. Pass an `apiKey` only,
|
|
16
|
+
> and Ablo keeps your rows in its own log so you can build the whole app today —
|
|
17
|
+
> like Stripe test mode. For a sandbox you can throw away, point Ablo at a separate
|
|
18
|
+
> or local Postgres. Connect your production database (below) when you're ready for
|
|
19
|
+
> it to be the system of record.
|
|
20
|
+
|
|
21
|
+
Connecting sets up two capabilities on your Postgres: **logical replication**, so
|
|
22
|
+
Ablo can read and confirm, and a **scoped DML role**, so Ablo can write. `ablo
|
|
23
|
+
connect` prints the exact SQL. `ablo connect apply` runs it for you.
|
|
24
|
+
|
|
25
|
+
## Connect in one command
|
|
11
26
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
> replication (below) when you're ready for it to be the system of record.
|
|
27
|
+
```bash
|
|
28
|
+
npx ablo connect apply --url postgres://admin:...@host:5432/db
|
|
29
|
+
```
|
|
16
30
|
|
|
17
|
-
|
|
18
|
-
|
|
31
|
+
Pass an admin connection string with `--url` and it creates the publication, the
|
|
32
|
+
two scoped roles, and the grants, turns on logical decoding where it can, registers
|
|
33
|
+
both scoped roles with Ablo, and proves the setup by reconnecting and reading back.
|
|
34
|
+
The admin credential is used on this machine only and never persisted — nothing is
|
|
35
|
+
written to your `.env`, which keeps holding only `ABLO_API_KEY`. Pass `--show-sql`
|
|
36
|
+
to see every statement first, or drop `--apply` to print the SQL and run it
|
|
37
|
+
yourself. Rotate the scoped passwords any time with `ablo connect rotate`.
|
|
19
38
|
|
|
20
|
-
|
|
39
|
+
The rest of this page is what that command sets up, step by step, for when you want
|
|
40
|
+
to run it by hand or review exactly what changes.
|
|
21
41
|
|
|
22
|
-
|
|
23
|
-
prints the exact SQL and validates it for you — you never hand-craft replication
|
|
24
|
-
internals.
|
|
42
|
+
## The setup, step by step
|
|
25
43
|
|
|
26
44
|
### 1. Enable logical decoding
|
|
27
45
|
|
|
28
|
-
Turn on logical WAL so Ablo can decode row changes:
|
|
46
|
+
Turn on logical WAL so Ablo can decode row changes and confirm writes:
|
|
29
47
|
|
|
30
48
|
```sql
|
|
31
49
|
ALTER SYSTEM SET wal_level = 'logical';
|
|
@@ -34,21 +52,20 @@ ALTER SYSTEM SET wal_level = 'logical';
|
|
|
34
52
|
`wal_level` is **not reloadable** — you must **restart Postgres** for it to take
|
|
35
53
|
effect. On Amazon RDS / Aurora you can't `ALTER SYSTEM`; set
|
|
36
54
|
`rds.logical_replication = 1` in the instance's parameter group instead, then
|
|
37
|
-
reboot.
|
|
55
|
+
reboot. (`ablo connect apply` attempts this for you and, where a managed
|
|
56
|
+
provider refuses, hands you the one remaining step.)
|
|
38
57
|
|
|
39
|
-
### 2. Run `ablo connect`
|
|
58
|
+
### 2. Run `ablo connect` for the publication and roles
|
|
40
59
|
|
|
41
60
|
```bash
|
|
42
61
|
npx ablo connect
|
|
43
62
|
```
|
|
44
63
|
|
|
45
|
-
`ablo connect` prints the exact, copy-pasteable setup SQL for **your** Postgres
|
|
46
|
-
|
|
47
|
-
a driver, because logical replication is how you connect. Run the printed SQL
|
|
48
|
-
against your database (as a superuser / the DB owner). It does three things:
|
|
64
|
+
`ablo connect` prints the exact, copy-pasteable setup SQL for **your** Postgres.
|
|
65
|
+
Run it against your database as a superuser or the DB owner. It creates:
|
|
49
66
|
|
|
50
|
-
- **A publication** naming the tables Ablo
|
|
51
|
-
single canonical name the runtime subscribes to):
|
|
67
|
+
- **A publication** naming the tables Ablo reads and confirms against
|
|
68
|
+
(`ablo_publication`, the single canonical name the runtime subscribes to):
|
|
52
69
|
|
|
53
70
|
```sql
|
|
54
71
|
CREATE PUBLICATION "ablo_publication" FOR ALL TABLES;
|
|
@@ -56,53 +73,79 @@ against your database (as a superuser / the DB owner). It does three things:
|
|
|
56
73
|
|
|
57
74
|
Scope it to a subset with `npx ablo connect --tables a,b,c`.
|
|
58
75
|
|
|
59
|
-
- **A
|
|
60
|
-
|
|
61
|
-
servers:
|
|
76
|
+
- **A replication role** — it streams the WAL and `SELECT`s, nothing more. This is
|
|
77
|
+
the role Ablo reads and confirms through. You choose the password; it never
|
|
78
|
+
passes through Ablo's CLI or servers:
|
|
62
79
|
|
|
63
80
|
```sql
|
|
64
81
|
CREATE ROLE "ablo_replicator" WITH REPLICATION LOGIN PASSWORD '<password>';
|
|
65
82
|
GRANT SELECT ON ALL TABLES IN SCHEMA public TO "ablo_replicator";
|
|
66
|
-
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO "ablo_replicator";
|
|
67
83
|
```
|
|
68
84
|
|
|
69
|
-
|
|
70
|
-
|
|
85
|
+
On Amazon RDS the `REPLICATION` attribute is granted, not set directly:
|
|
86
|
+
`GRANT rds_replication TO "ablo_replicator";`.
|
|
87
|
+
|
|
88
|
+
- **A scoped writer role** — the role Ablo writes your rows through. It gets row
|
|
89
|
+
DML (`SELECT, INSERT, UPDATE, DELETE`) and the sync ledger, and nothing else: no
|
|
90
|
+
`REPLICATION`, no schema `CREATE`, `NOSUPERUSER NOBYPASSRLS`, row security on. It
|
|
91
|
+
can change rows in your tables; it cannot change your database:
|
|
92
|
+
|
|
93
|
+
```sql
|
|
94
|
+
CREATE ROLE "ablo_writer" WITH LOGIN PASSWORD '<write-password>'
|
|
95
|
+
NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE NOREPLICATION NOINHERIT;
|
|
96
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "ablo_writer";
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Rename either role with `--role <name>` / `--write-role <name>`.
|
|
71
100
|
|
|
72
101
|
The **replication slot** is created and owned by Ablo's runtime when it first
|
|
73
|
-
subscribes
|
|
74
|
-
are the only objects the recipe asks you to create.
|
|
102
|
+
subscribes — you don't pre-create it.
|
|
75
103
|
|
|
76
|
-
### 3.
|
|
104
|
+
### 3. Register the database with Ablo
|
|
77
105
|
|
|
78
|
-
|
|
79
|
-
|
|
106
|
+
`ablo connect apply` already did this. If you ran the SQL by hand instead, hand
|
|
107
|
+
Ablo both scoped connection strings once — the replication role it reads and
|
|
108
|
+
confirms through, and the writer role it lands your rows through. Set them just
|
|
109
|
+
long enough to register:
|
|
80
110
|
|
|
81
111
|
```bash
|
|
82
|
-
|
|
112
|
+
export ABLO_REPLICATION_DATABASE_URL=... # the replication role
|
|
113
|
+
export ABLO_WRITE_DATABASE_URL=... # the writer role
|
|
114
|
+
npx ablo connect register
|
|
83
115
|
```
|
|
84
116
|
|
|
85
|
-
|
|
86
|
-
|
|
117
|
+
`--register` posts them to Ablo, which holds them encrypted and uses them to read
|
|
118
|
+
and write your database. Ablo holds them from here, so you can drop both from your
|
|
119
|
+
environment — your app keeps only `ABLO_API_KEY`. The role passwords are generated
|
|
120
|
+
for you and never printed; rotate them any time with `ablo connect rotate`. After
|
|
121
|
+
this, Ablo does all the connecting.
|
|
122
|
+
|
|
123
|
+
### 4. Verify readiness with `ablo connect check`
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npx ablo connect check
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`--check` needs only `ABLO_API_KEY`. It asks Ablo to check the database it now
|
|
130
|
+
holds, from the same infrastructure replication runs on, and prints a green
|
|
131
|
+
checklist or the precise per-item fix:
|
|
87
132
|
|
|
88
133
|
- `wal_level` is `logical`
|
|
89
134
|
- the `ablo_publication` publication exists
|
|
90
|
-
- the
|
|
135
|
+
- the replication role has the `REPLICATION` attribute
|
|
91
136
|
- every published table has a usable `REPLICA IDENTITY` (a primary key, or
|
|
92
137
|
`REPLICA IDENTITY FULL`) so `UPDATE`/`DELETE` can replicate
|
|
138
|
+
- the writer role is DML-ready — scoped, non-superuser, with the idempotency
|
|
139
|
+
ledger in place
|
|
93
140
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
141
|
+
Because Ablo checks from its own network, a database your own machine can't reach —
|
|
142
|
+
IPv6-only, IP-allowlisted, behind a VPN — still verifies. Re-run it until every
|
|
143
|
+
item is green.
|
|
97
144
|
|
|
98
|
-
|
|
99
|
-
`REPLICATION`-attributed role that streams the WAL and `SELECT`s, nothing more
|
|
100
|
-
(not a read-only account; see the privilege note below). The same value `--check`
|
|
101
|
-
validated:
|
|
145
|
+
Your **app** holds only the API key — never a connection string:
|
|
102
146
|
|
|
103
147
|
```bash
|
|
104
148
|
# .env — server runtime only, never the browser
|
|
105
|
-
DATABASE_URL=postgres://ablo_replicator:<password>@host:5432/db?sslmode=require
|
|
106
149
|
ABLO_API_KEY=sk_live_...
|
|
107
150
|
```
|
|
108
151
|
|
|
@@ -116,28 +159,33 @@ export const ablo = Ablo({
|
|
|
116
159
|
});
|
|
117
160
|
```
|
|
118
161
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
and
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
owned by you," which is exactly right.
|
|
162
|
+
The Ablo schema describes **only your synced, collaborative models** — the rows
|
|
163
|
+
Ablo coordinates and fans out in realtime. It is *not* your whole-database schema
|
|
164
|
+
and does *not* replace your `schema.prisma` (or Drizzle schema). Your auth,
|
|
165
|
+
billing, and other tables stay in your own ORM schema, owned by your own
|
|
166
|
+
migrations. `ablo check` reflects this — it reports tables you didn't declare as
|
|
167
|
+
"ignored / owned by you," which is exactly right.
|
|
126
168
|
|
|
127
|
-
### 5.
|
|
169
|
+
### 5. Write through `ablo.<model>`
|
|
128
170
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
The read, claim, and coordination surface (`ablo.<model>`) layers on top:
|
|
171
|
+
Every change goes through Ablo. The write enters the commit chokepoint, Ablo
|
|
172
|
+
applies it to your Postgres through the writer role, and the WAL echo confirms it
|
|
173
|
+
landed:
|
|
133
174
|
|
|
134
175
|
```ts
|
|
176
|
+
// Enters the chokepoint (claims, ordering, idempotency), lands in your Postgres.
|
|
177
|
+
await ablo.weatherReports.update({ id: 'report_stockholm', data: { high: 21 } });
|
|
178
|
+
|
|
179
|
+
// Block until your database has it and the WAL echo confirms.
|
|
180
|
+
await ablo.weatherReports.update({ id: 'report_stockholm', data: { high: 21 }, wait: 'confirmed' });
|
|
181
|
+
|
|
182
|
+
// Reads are live off the same stream.
|
|
135
183
|
const report = ablo.weatherReports.get('report_stockholm');
|
|
136
|
-
const active = ablo.weatherReports.claim.state({ id: 'report_stockholm' });
|
|
137
184
|
```
|
|
138
185
|
|
|
139
|
-
|
|
140
|
-
[
|
|
186
|
+
A commit is accepted the moment Ablo takes it (`queued`); it becomes `confirmed`
|
|
187
|
+
once the row appears on your WAL. See [Guarantees](./guarantees.md) for what each
|
|
188
|
+
state means and when to wait.
|
|
141
189
|
|
|
142
190
|
## What Ablo touches in your database — the honest footprint
|
|
143
191
|
|
|
@@ -145,9 +193,10 @@ This is the complete list. Nothing else.
|
|
|
145
193
|
|
|
146
194
|
| Object | What it is | Owned by |
|
|
147
195
|
|---|---|---|
|
|
148
|
-
| `ablo_publication` | A
|
|
196
|
+
| `ablo_publication` | A publication naming the tables Ablo reads and confirms against. | You create it (step 2). |
|
|
197
|
+
| `ablo_replicator` role | A `REPLICATION` + `SELECT` role Ablo reads and confirms through. | You create it (step 2). |
|
|
198
|
+
| `ablo_writer` role | A scoped DML role Ablo writes your rows through — row DML + ledger, nothing more. | You create it (step 2). |
|
|
149
199
|
| Replication slot | A logical slot Ablo subscribes through to track its WAL position. | Ablo's runtime creates it on first connect. |
|
|
150
|
-
| `ablo_replicator` role | A least-privilege `REPLICATION` + `SELECT` role. | You create it (step 2). |
|
|
151
200
|
| `wal_level = logical` | A server setting that **requires a restart**. | You set it (step 1). |
|
|
152
201
|
|
|
153
202
|
Operational reality you should know up front:
|
|
@@ -155,30 +204,26 @@ Operational reality you should know up front:
|
|
|
155
204
|
- **`wal_level = logical` needs a restart.** It is a one-time, server-wide change
|
|
156
205
|
and is not reloadable.
|
|
157
206
|
- **A replication slot retains WAL.** While Ablo is connected, the slot holds the
|
|
158
|
-
WAL it hasn't yet acknowledged. If Ablo is disconnected for a long time, that
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
- **The role
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
- It **never runs DDL** against your database.
|
|
172
|
-
- It **never owns or migrates your schema** — your migration tool stays in charge.
|
|
173
|
-
- It **never writes your rows** — writes are yours, through your backend.
|
|
207
|
+
WAL it hasn't yet acknowledged. If Ablo is disconnected for a long time, that WAL
|
|
208
|
+
accumulates and consumes disk. **Ablo monitors slot lag and WAL retention** and
|
|
209
|
+
surfaces it, so disk pressure never surprises you; an abandoned slot is dropped
|
|
210
|
+
rather than left to grow unbounded.
|
|
211
|
+
- **The writer role changes rows, not your database.** It carries row DML plus the
|
|
212
|
+
sync ledger and nothing more — no `REPLICATION`, no DDL, no object ownership, and
|
|
213
|
+
it runs with row security on and `NOBYPASSRLS`. It is a real, tightly-scoped
|
|
214
|
+
privilege — describe it that way in a security review.
|
|
215
|
+
|
|
216
|
+
Ablo runs **no DDL** and **owns no schema**: your migration tool stays in charge of
|
|
217
|
+
the shape of your database, and Ablo writes only rows, only into tables you already
|
|
218
|
+
have.
|
|
174
219
|
|
|
175
220
|
## What Ablo stores on its side
|
|
176
221
|
|
|
177
222
|
Your schema *definition* (model names, fields, types — pushed with `ablo push`),
|
|
178
223
|
your hashed API keys, a safe projection of the connection registration (host,
|
|
179
224
|
database, schema — the connection string itself is sealed and never echoed back),
|
|
180
|
-
the replication slot position, and the
|
|
181
|
-
rows.
|
|
225
|
+
the replication slot position, and the ordered transaction log that drives sync and
|
|
226
|
+
coordination. Your rows live in your database.
|
|
182
227
|
|
|
183
228
|
> **Postgres replication status: Preview.** Registration, readiness checks, and
|
|
184
229
|
> the server replication fleet are implemented and boot-wired. Preview describes
|
|
@@ -186,6 +231,17 @@ rows.
|
|
|
186
231
|
> [internal/postgres-replication.md](./internal/postgres-replication.md) for the
|
|
187
232
|
> architecture and operational invariants.
|
|
188
233
|
|
|
234
|
+
## When your database can't grant replication
|
|
235
|
+
|
|
236
|
+
Some managed databases won't grant a `REPLICATION` role. For those, Ablo connects
|
|
237
|
+
through a **signed Data Source endpoint** instead: you expose one signed HTTP route
|
|
238
|
+
built from your ORM (`prismaDataSource` / `drizzleDataSource`, with `ablo_outbox` /
|
|
239
|
+
`ablo_idempotency` bookkeeping), and Ablo writes and confirms through it — same
|
|
240
|
+
`ablo.<model>` surface, same commit chokepoint, same `queued` → `confirmed`
|
|
241
|
+
lifecycle. It needs no replication setup, which is exactly why it's the fallback:
|
|
242
|
+
reach for it only when logical replication isn't available, and prefer `ablo
|
|
243
|
+
connect` everywhere else.
|
|
244
|
+
|
|
189
245
|
## Next steps
|
|
190
246
|
|
|
191
247
|
- [Quickstart](./quickstart.md) — connect and write through `ablo.<model>`.
|
|
@@ -194,20 +250,3 @@ rows.
|
|
|
194
250
|
- [Guarantees](./guarantees.md) — what confirmed writes and stale checks mean.
|
|
195
251
|
- [Integration Guide](./integration-guide.md) — the full app, React, multiplayer,
|
|
196
252
|
and agent path.
|
|
197
|
-
|
|
198
|
-
---
|
|
199
|
-
|
|
200
|
-
## Legacy / not recommended
|
|
201
|
-
|
|
202
|
-
> **Use logical replication instead** (top of this page). The shapes below
|
|
203
|
-
> predate the single connect path. They are documented only so existing
|
|
204
|
-
> integrations can be read and understood — do **not** reach for them when
|
|
205
|
-
> connecting a new database. They are the seams that caused painful onboarding,
|
|
206
|
-
> and `ablo connect` exists precisely to replace them.
|
|
207
|
-
|
|
208
|
-
This older shape connected Ablo to a database by exposing a **signed Data Source
|
|
209
|
-
endpoint** built from an ORM adapter (`prismaDataSource` / `drizzleDataSource`,
|
|
210
|
-
with `ablo_outbox` / `ablo_idempotency` bookkeeping and a reverse-channel
|
|
211
|
-
connector for VPCs). It required Ablo to proxy every write, and has been superseded
|
|
212
|
-
by logical replication, where Ablo only reads your WAL. If you are maintaining one
|
|
213
|
-
of these integrations, migrate it to `ablo connect` at the next opportunity.
|
|
@@ -50,11 +50,11 @@ export async function markReady(reportId: string) {
|
|
|
50
50
|
// queue: false → don't queue behind a current holder. If a human already
|
|
51
51
|
// holds the row, claim rejects with AbloClaimedError (caught below), so the
|
|
52
52
|
// agent yields instead of waiting. Omit it, or pass queue: true, to queue
|
|
53
|
-
// behind them.
|
|
53
|
+
// behind them. description → the label observers see while we work.
|
|
54
54
|
await using claim = await ablo.weatherReports.claim({
|
|
55
55
|
id: reportId,
|
|
56
56
|
queue: false,
|
|
57
|
-
|
|
57
|
+
description: 'marking_ready',
|
|
58
58
|
});
|
|
59
59
|
const claimed = claim.data;
|
|
60
60
|
|
|
@@ -43,7 +43,7 @@ const updateReport = tool({
|
|
|
43
43
|
// The claim is released automatically when it goes out of scope.
|
|
44
44
|
await using claim = await ablo.weatherReports.claim({
|
|
45
45
|
id: reportId,
|
|
46
|
-
|
|
46
|
+
description: 'editing',
|
|
47
47
|
ttl: '2m',
|
|
48
48
|
});
|
|
49
49
|
const claimed = claim.data;
|
package/docs/examples/nextjs.md
CHANGED
|
@@ -39,7 +39,7 @@ export async function completeReport(reportId: string) {
|
|
|
39
39
|
await using claim = await ablo.weatherReports.claim({
|
|
40
40
|
id: reportId,
|
|
41
41
|
queue: false,
|
|
42
|
-
|
|
42
|
+
description: 'completing',
|
|
43
43
|
});
|
|
44
44
|
const claimed = claim.data;
|
|
45
45
|
|
|
@@ -62,7 +62,7 @@ The two options on the claim:
|
|
|
62
62
|
|
|
63
63
|
- `queue: false` — skip this record if another claim is already in progress,
|
|
64
64
|
rather than queueing behind it. (The default queues.)
|
|
65
|
-
- `
|
|
65
|
+
- `description: 'completing'` — a human-readable label for what your worker is doing,
|
|
66
66
|
visible to anyone reading `claim.state({ id })`.
|
|
67
67
|
|
|
68
68
|
Because the worker uses the same schema and `claim()` as the UI, its writes sync
|