@abloatai/ablo 0.16.3 → 0.17.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/CHANGELOG.md +34 -0
- package/dist/BaseSyncedStore.d.ts +3 -1
- package/dist/BaseSyncedStore.js +27 -4
- package/dist/cli.cjs +366 -211
- package/dist/client/Ablo.js +24 -11
- package/dist/schema/diff.d.ts +14 -0
- package/docs/data-sources.md +164 -287
- package/docs/quickstart.md +51 -63
- package/package.json +1 -1
package/dist/client/Ablo.js
CHANGED
|
@@ -518,17 +518,30 @@ function createDynamicModelClass(modelName, jsonSubFields, fieldNames, computed,
|
|
|
518
518
|
}
|
|
519
519
|
return ModelClass;
|
|
520
520
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
521
|
+
const LOG_LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40, silent: 99 };
|
|
522
|
+
function resolveLogLevel() {
|
|
523
|
+
// `globalThis.process` guard keeps this safe in browser/edge runtimes that
|
|
524
|
+
// have no `process` binding — there we fall through to the default.
|
|
525
|
+
const raw = globalThis.process?.env?.ABLO_LOG_LEVEL;
|
|
526
|
+
const normalized = raw?.toLowerCase();
|
|
527
|
+
if (normalized && normalized in LOG_LEVEL_RANK)
|
|
528
|
+
return normalized;
|
|
529
|
+
return 'warn';
|
|
530
|
+
}
|
|
531
|
+
const consoleLogger = (() => {
|
|
532
|
+
const threshold = LOG_LEVEL_RANK[resolveLogLevel()];
|
|
533
|
+
const emit = (level, fn, args) => {
|
|
534
|
+
if (typeof console === 'undefined' || LOG_LEVEL_RANK[level] < threshold)
|
|
535
|
+
return;
|
|
536
|
+
fn('[sync]', ...args);
|
|
537
|
+
};
|
|
538
|
+
return {
|
|
539
|
+
debug: (...args) => emit('debug', console.debug, args),
|
|
540
|
+
info: (...args) => emit('info', console.info, args),
|
|
541
|
+
warn: (...args) => emit('warn', console.warn, args),
|
|
542
|
+
error: (...args) => emit('error', console.error, args),
|
|
543
|
+
};
|
|
544
|
+
})();
|
|
532
545
|
// `readProcessEnv` lives in `./auth` alongside the other resolvers
|
|
533
546
|
// that read it. Re-exported there for use elsewhere in the file.
|
|
534
547
|
// ── Default mutation executor (wire: `commit` frame over WebSocket) ──────
|
package/dist/schema/diff.d.ts
CHANGED
|
@@ -133,6 +133,20 @@ export interface MigrationSignal {
|
|
|
133
133
|
readonly model: string;
|
|
134
134
|
readonly field?: string;
|
|
135
135
|
readonly detail: string;
|
|
136
|
+
/**
|
|
137
|
+
* Reader-visibility context for a removal that shadows an existing artifact:
|
|
138
|
+
* the active schema this push is being diffed against. Lets the CLI show the
|
|
139
|
+
* baseline — version + WHEN it was pushed — so "incompatible" isn't a mystery
|
|
140
|
+
* (e.g. a first sandbox push diffed against a months-old production schema).
|
|
141
|
+
*/
|
|
142
|
+
readonly shadowed?: {
|
|
143
|
+
readonly environment: string;
|
|
144
|
+
readonly version: number;
|
|
145
|
+
/** ISO timestamp the shadowed artifact was activated/pushed, or null. */
|
|
146
|
+
readonly pushedAt: string | null;
|
|
147
|
+
/** Who pushed it (e.g. `apikey:…`), or null. */
|
|
148
|
+
readonly pushedBy: string | null;
|
|
149
|
+
};
|
|
136
150
|
}
|
|
137
151
|
export interface MigrationClassification {
|
|
138
152
|
/** Execute but may lose or risk data on a non-empty table. */
|
package/docs/data-sources.md
CHANGED
|
@@ -1,335 +1,212 @@
|
|
|
1
1
|
# Connect Your Database
|
|
2
2
|
|
|
3
|
-
**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
> Just trying Ablo? You don't need a database at all to start: the hosted
|
|
3
|
+
**Connect your database = Postgres logical replication.** That is the one way.
|
|
4
|
+
Ablo reads your write-ahead log (WAL) and **never runs DDL, never owns your
|
|
5
|
+
schema, and never migrates it**. Your application keeps writing to its own
|
|
6
|
+
Postgres through its own backend, exactly as it does today; Ablo only tails the
|
|
7
|
+
changes and fans the confirmed rows out to every connected human and agent. This
|
|
8
|
+
is the same model ElectricSQL, PowerSync, and Zero use — a publication plus a
|
|
9
|
+
replication slot, read-only.
|
|
10
|
+
|
|
11
|
+
> **Just trying Ablo?** You don't need a database at all to start. The hosted
|
|
14
12
|
> **sandbox** can host rows in Ablo's test plane — pass an `apiKey` only and omit
|
|
15
|
-
>
|
|
16
|
-
> below) when you're ready for it to be the system of record.
|
|
13
|
+
> any database setup, like Stripe test mode. Connect your Postgres with logical
|
|
14
|
+
> replication (below) when you're ready for it to be the system of record.
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
coordinates and fans out in realtime. It is *not* your whole-database schema and
|
|
21
|
-
does *not* replace your `schema.prisma` (or your Drizzle schema). Your auth,
|
|
22
|
-
billing, and any other non-synced tables stay in your own ORM schema, owned by
|
|
23
|
-
your own migrations. One database, two schemas, side by side: Ablo owns the
|
|
24
|
-
synced models; you keep owning everything else. `ablo check` reflects this — it
|
|
25
|
-
reports your other tables as "ignored / owned by you," which is exactly right.
|
|
16
|
+
Your database stays the system of record. Ablo never becomes a second source of
|
|
17
|
+
truth and never takes over operating your Postgres.
|
|
26
18
|
|
|
27
|
-
|
|
28
|
-
types — pushed with `ablo push`), your hashed API keys, a safe projection of the
|
|
29
|
-
connection registration (host, database, schema — the connection string itself
|
|
30
|
-
is sealed and never echoed back), and the commit log that drives sync. Never
|
|
31
|
-
your rows.
|
|
19
|
+
## The five steps (mirrors Zero's install flow)
|
|
32
20
|
|
|
33
|
-
|
|
21
|
+
You run the setup once against your own database, then point Ablo at it. The CLI
|
|
22
|
+
prints the exact SQL and validates it for you — you never hand-craft replication
|
|
23
|
+
internals.
|
|
34
24
|
|
|
35
|
-
|
|
25
|
+
### 1. Enable logical decoding
|
|
36
26
|
|
|
37
|
-
|
|
38
|
-
import Ablo from '@abloatai/ablo';
|
|
39
|
-
import { schema } from './ablo/schema';
|
|
27
|
+
Turn on logical WAL so Ablo can decode row changes:
|
|
40
28
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
apiKey: process.env.ABLO_API_KEY,
|
|
44
|
-
databaseUrl: process.env.DATABASE_URL, // your Postgres, passed explicitly — rows live here
|
|
45
|
-
});
|
|
29
|
+
```sql
|
|
30
|
+
ALTER SYSTEM SET wal_level = 'logical';
|
|
46
31
|
```
|
|
47
32
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
On first connect the SDK registers the connection — sent once over TLS, stored
|
|
55
|
-
sealed, never returned by any API. From then on Ablo commits every confirmed
|
|
56
|
-
write directly to your database and reads canonical rows from it.
|
|
57
|
-
|
|
58
|
-
### A localhost Postgres can't be the system of record
|
|
59
|
-
|
|
60
|
-
This is the connection-string fact people hit first. Ablo's **cloud** registers
|
|
61
|
-
your connection string and connects to your Postgres **over the network**. A
|
|
62
|
-
`localhost` / private-range database (`127.0.0.1`, `192.168.*`, Docker's
|
|
63
|
-
`db:5432`) is unreachable from Ablo's side, so such connection strings are
|
|
64
|
-
**rejected**. Two escape hatches for local development against your own DB:
|
|
65
|
-
|
|
66
|
-
- **Expose a signed Data Source endpoint.** Your app — which *can* reach your
|
|
67
|
-
local DB — proxies Ablo's commits to it. See [Signed Endpoint](#signed-endpoint)
|
|
68
|
-
below. This is the right answer for "my dev DB stays on my machine."
|
|
69
|
-
- **Use the hosted sandbox.** Skip the database entirely: pass an `apiKey` only,
|
|
70
|
-
omit `databaseUrl`, and let Ablo's test plane host the rows while you build.
|
|
71
|
-
|
|
72
|
-
Safety requirements, enforced server-side before the first write:
|
|
33
|
+
`wal_level` is **not reloadable** — you must **restart Postgres** for it to take
|
|
34
|
+
effect. On Amazon RDS / Aurora you can't `ALTER SYSTEM`; set
|
|
35
|
+
`rds.logical_replication = 1` in the instance's parameter group instead, then
|
|
36
|
+
reboot.
|
|
73
37
|
|
|
74
|
-
|
|
75
|
-
`BYPASSRLS` — Ablo's tenant isolation is row-level security, and a role that
|
|
76
|
-
can bypass it is rejected outright.
|
|
77
|
-
- **Row-level security on synced tables.** `npx ablo migrate` provisions your
|
|
78
|
-
synced-model tables with `FORCE ROW LEVEL SECURITY` already applied; tables
|
|
79
|
-
you create yourself must do the same.
|
|
80
|
-
- **Network-reachable host.** As above, connection strings resolving to loopback
|
|
81
|
-
or private address ranges are rejected — Ablo connects from its cloud.
|
|
38
|
+
### 2. Run `ablo connect` to get the publication / slot / role SQL
|
|
82
39
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
never auto-read from the environment — pass it explicitly to `Ablo(...)`.
|
|
86
|
-
|
|
87
|
-
## Signed Endpoint
|
|
88
|
-
|
|
89
|
-
When a connection string must not leave your infrastructure, keep
|
|
90
|
-
`DATABASE_URL` in your app and expose one HTTPS endpoint instead. Ablo signs a
|
|
91
|
-
commit request; an ORM adapter in your route runs it in one transaction against
|
|
92
|
-
your Postgres and returns the canonical rows. Omit `databaseUrl` from
|
|
93
|
-
`Ablo(...)` in this setup — the client takes only the schema and the API key:
|
|
94
|
-
|
|
95
|
-
```ts
|
|
96
|
-
export const ablo = Ablo({
|
|
97
|
-
schema,
|
|
98
|
-
apiKey: process.env.ABLO_API_KEY,
|
|
99
|
-
});
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
The SDK call is identical in both shapes:
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
await ablo.weatherReports.create({ data: { location: 'Stockholm', status: 'pending' } });
|
|
106
|
-
await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' } });
|
|
107
|
-
const report = ablo.weatherReports.get('report_stockholm');
|
|
40
|
+
```bash
|
|
41
|
+
npx ablo connect
|
|
108
42
|
```
|
|
109
43
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
automatically. To keep everyone's screen up to date, your app reports those
|
|
115
|
-
outside changes back through the outbox feed — shown below in
|
|
116
|
-
[Outbox Events](#outbox-events).
|
|
44
|
+
`ablo connect` prints the exact, copy-pasteable setup SQL for **your** Postgres
|
|
45
|
+
and nothing else — it does not ask for a connection-string flavor, an adapter, or
|
|
46
|
+
a driver, because logical replication is how you connect. Run the printed SQL
|
|
47
|
+
against your database (as a superuser / the DB owner). It does three things:
|
|
117
48
|
|
|
118
|
-
|
|
49
|
+
- **A publication** naming the tables Ablo should read (`ablo_publication`, the
|
|
50
|
+
single canonical name the runtime subscribes to):
|
|
119
51
|
|
|
120
|
-
|
|
121
|
-
|
|
52
|
+
```sql
|
|
53
|
+
CREATE PUBLICATION "ablo_publication" FOR ALL TABLES;
|
|
54
|
+
```
|
|
122
55
|
|
|
123
|
-
|
|
124
|
-
read [Integration Guide](./integration-guide.md) first, then
|
|
125
|
-
[Existing Python Backend](./examples/existing-python-backend.md) for a concrete
|
|
126
|
-
service-owned database example.
|
|
56
|
+
Scope it to a subset with `npx ablo connect --tables a,b,c`.
|
|
127
57
|
|
|
128
|
-
|
|
58
|
+
- **A least-privilege replication role** — it can stream replication and `SELECT`,
|
|
59
|
+
nothing more. You choose the password; it never passes through Ablo's CLI or
|
|
60
|
+
servers:
|
|
129
61
|
|
|
130
|
-
|
|
62
|
+
```sql
|
|
63
|
+
CREATE ROLE "ablo_replicator" WITH REPLICATION LOGIN PASSWORD '<password>';
|
|
64
|
+
GRANT SELECT ON ALL TABLES IN SCHEMA public TO "ablo_replicator";
|
|
65
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO "ablo_replicator";
|
|
66
|
+
```
|
|
131
67
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
| Data Source endpoint | The public HTTPS endpoint in your app that Ablo calls. |
|
|
135
|
-
| API key | Stored in your app as `ABLO_API_KEY`; used by the SDK and the Data Source endpoint. |
|
|
136
|
-
| External-write feed | Optional `events` handler on the same Data Source endpoint. |
|
|
137
|
-
| Status | Last successful request, last error, and delivery attempts. |
|
|
68
|
+
Rename it with `--role <name>`. On Amazon RDS the `REPLICATION` attribute is
|
|
69
|
+
granted, not set directly: `GRANT rds_replication TO "ablo_replicator";`.
|
|
138
70
|
|
|
139
|
-
The
|
|
71
|
+
The **replication slot** is created and owned by Ablo's runtime when it first
|
|
72
|
+
subscribes with this role — you don't pre-create it. The publication and the role
|
|
73
|
+
are the only objects the recipe asks you to create.
|
|
140
74
|
|
|
141
|
-
|
|
142
|
-
2. Store `ABLO_API_KEY` in your app.
|
|
143
|
-
3. Verify signed HTTP calls before opening a database transaction.
|
|
144
|
-
4. Keep your database credentials in your app.
|
|
145
|
-
5. Write an outbox row in the same transaction as every app-row change.
|
|
146
|
-
|
|
147
|
-
## Route
|
|
148
|
-
|
|
149
|
-
You don't hand-write the commit transaction, the idempotency upsert, or the
|
|
150
|
-
outbox writes. You pass an ORM **adapter** and it does all of that for you —
|
|
151
|
-
transaction, exactly-once idempotency, and outbox — driven by the same Ablo
|
|
152
|
-
schema. The whole route is three fields:
|
|
153
|
-
|
|
154
|
-
```ts
|
|
155
|
-
// app/api/ablo/source/route.ts
|
|
156
|
-
import { dataSourceNext } from '@abloatai/ablo/source/next';
|
|
157
|
-
import { prismaDataSource } from '@abloatai/ablo/source';
|
|
158
|
-
import { schema } from '@/ablo/schema';
|
|
159
|
-
import { prisma } from '@/lib/prisma';
|
|
75
|
+
### 3. Validate with `ablo connect --check`
|
|
160
76
|
|
|
161
|
-
|
|
162
|
-
|
|
77
|
+
Put the replication role's connection string in `DATABASE_URL`, then verify the
|
|
78
|
+
database is replication-ready:
|
|
163
79
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
apiKey: process.env.ABLO_API_KEY!,
|
|
167
|
-
adapter: prismaDataSource(prisma, schema),
|
|
168
|
-
});
|
|
80
|
+
```bash
|
|
81
|
+
npx ablo connect --check
|
|
169
82
|
```
|
|
170
83
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
```ts
|
|
175
|
-
// app/api/ablo/source/route.ts
|
|
176
|
-
import { dataSourceNext } from '@abloatai/ablo/source/next';
|
|
177
|
-
import { drizzleDataSource } from '@abloatai/ablo/source/drizzle';
|
|
178
|
-
import { schema } from '@/ablo/schema';
|
|
179
|
-
import { db } from '@/db';
|
|
84
|
+
This connects and checks the four invariants, printing a green checklist or the
|
|
85
|
+
precise per-item fix:
|
|
180
86
|
|
|
181
|
-
|
|
87
|
+
- `wal_level` is `logical`
|
|
88
|
+
- the `ablo_publication` publication exists
|
|
89
|
+
- the `DATABASE_URL` role has the `REPLICATION` attribute
|
|
90
|
+
- every published table has a usable `REPLICA IDENTITY` (a primary key, or
|
|
91
|
+
`REPLICA IDENTITY FULL`) so `UPDATE`/`DELETE` can replicate
|
|
182
92
|
|
|
183
|
-
|
|
184
|
-
schema,
|
|
185
|
-
apiKey: process.env.ABLO_API_KEY!,
|
|
186
|
-
adapter: drizzleDataSource(db, schema),
|
|
187
|
-
});
|
|
188
|
-
```
|
|
93
|
+
Re-run it until every item is green.
|
|
189
94
|
|
|
190
|
-
|
|
191
|
-
`prismaDataSource(prisma, schema)` or `drizzleDataSource(db, schema)`. It maps
|
|
192
|
-
each synced model to your table, wraps every commit in one transaction, dedupes
|
|
193
|
-
on `clientTxId` via `ablo_idempotency`, and appends `ablo_outbox` rows for the
|
|
194
|
-
external-write feed — the bookkeeping you used to write by hand.
|
|
95
|
+
### 4. Point Ablo at the database with the replication role
|
|
195
96
|
|
|
196
|
-
|
|
97
|
+
Give Ablo the connection string for the **replication role** you created. This is
|
|
98
|
+
a read-only WAL connection — the same value `--check` validated:
|
|
197
99
|
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
wait: 'confirmed',
|
|
203
|
-
readAt: snap.stamp,
|
|
204
|
-
onStale: 'reject',
|
|
205
|
-
});
|
|
100
|
+
```bash
|
|
101
|
+
# .env — server runtime only, never the browser
|
|
102
|
+
DATABASE_URL=postgres://ablo_replicator:<password>@host:5432/db?sslmode=require
|
|
103
|
+
ABLO_API_KEY=sk_live_...
|
|
206
104
|
```
|
|
207
105
|
|
|
208
|
-
## Local development & locked-down VPC — the reverse channel
|
|
209
|
-
|
|
210
|
-
The signed route above is an **inbound webhook**: Ablo Cloud calls your HTTPS
|
|
211
|
-
endpoint. That needs a public URL — which `localhost` doesn't have, and a
|
|
212
|
-
locked-down VPC won't expose. The reverse channel fixes both. A connector you run
|
|
213
|
-
next to your database dials an **outbound** WebSocket to Ablo Cloud and serves the
|
|
214
|
-
same `commit`/`load`/`list` requests over it — the Stripe-CLI `stripe listen`
|
|
215
|
-
pattern. No tunnel, no public endpoint, and your database credentials never leave
|
|
216
|
-
your process.
|
|
217
|
-
|
|
218
|
-
It wraps the **same handler** your deployed route uses — share the options object,
|
|
219
|
-
so there's zero handler drift:
|
|
220
|
-
|
|
221
106
|
```ts
|
|
222
|
-
|
|
223
|
-
import {
|
|
224
|
-
import { sourceOptions } from '@/ablo/source'; // the same object route.ts passes
|
|
107
|
+
import Ablo from '@abloatai/ablo';
|
|
108
|
+
import { schema } from './ablo/schema';
|
|
225
109
|
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
110
|
+
export const ablo = Ablo({
|
|
111
|
+
schema,
|
|
112
|
+
apiKey: process.env.ABLO_API_KEY,
|
|
229
113
|
});
|
|
230
|
-
|
|
231
|
-
const controller = new AbortController();
|
|
232
|
-
await connector.run(controller.signal); // dials out, serves until aborted
|
|
233
114
|
```
|
|
234
115
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
116
|
+
You define an Ablo schema with `defineSchema`, `model`, and Zod. The Ablo schema
|
|
117
|
+
describes **only your synced, collaborative models** — the rows Ablo coordinates
|
|
118
|
+
and fans out in realtime. It is *not* your whole-database schema and does *not*
|
|
119
|
+
replace your `schema.prisma` (or your Drizzle schema). Your auth, billing, and
|
|
120
|
+
any other tables stay in your own ORM schema, owned by your own migrations.
|
|
121
|
+
`ablo check` reflects this — it reports tables you didn't declare as "ignored /
|
|
122
|
+
owned by you," which is exactly right.
|
|
240
123
|
|
|
241
|
-
|
|
242
|
-
`sk_test_*` keys (the dev-loop affordance). A customer who genuinely cannot expose
|
|
243
|
-
an inbound endpoint — a locked-down VPC — can run the connector as their deployed
|
|
244
|
-
production transport by opting that source into `reverseChannelProd` and using an
|
|
245
|
-
`sk_live_*` key. The inbound webhook remains the default for everyone else (it's
|
|
246
|
-
stateless and lower-latency); the reverse channel is the escape hatch for
|
|
247
|
-
no-inbound environments.
|
|
124
|
+
### 5. Writes go through your own backend
|
|
248
125
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
126
|
+
Your application writes to its Postgres the way it always has — its own ORM, its
|
|
127
|
+
own backend, its own transactions. Ablo does not intercept or proxy those writes.
|
|
128
|
+
It observes them on the WAL and fans the confirmed rows out to connected clients.
|
|
129
|
+
The read, claim, and coordination surface (`ablo.<model>`) layers on top:
|
|
252
130
|
|
|
253
131
|
```ts
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
clientTxId: 'tx_...',
|
|
257
|
-
operations: [
|
|
258
|
-
{
|
|
259
|
-
type: 'UPDATE',
|
|
260
|
-
model: 'weatherReports',
|
|
261
|
-
id: 'report_stockholm',
|
|
262
|
-
input: { status: 'ready' },
|
|
263
|
-
readAt: 1042,
|
|
264
|
-
onStale: 'reject',
|
|
265
|
-
},
|
|
266
|
-
],
|
|
267
|
-
scope: {
|
|
268
|
-
participantId: 'agent:triage',
|
|
269
|
-
participantKind: 'agent',
|
|
270
|
-
organizationId: 'org_123',
|
|
271
|
-
requiredSyncGroups: ['org:org_123'],
|
|
272
|
-
mode: 'live',
|
|
273
|
-
},
|
|
274
|
-
}
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
Return canonical rows:
|
|
278
|
-
|
|
279
|
-
```ts
|
|
280
|
-
{
|
|
281
|
-
rows: [
|
|
282
|
-
{ id: 'report_stockholm', location: 'Stockholm', status: 'ready' },
|
|
283
|
-
],
|
|
284
|
-
}
|
|
132
|
+
const report = ablo.weatherReports.get('report_stockholm');
|
|
133
|
+
const active = ablo.weatherReports.claim.state({ id: 'report_stockholm' });
|
|
285
134
|
```
|
|
286
135
|
|
|
287
|
-
|
|
288
|
-
|
|
136
|
+
For the typed read/claim/write surface itself, see
|
|
137
|
+
[Quickstart](./quickstart.md) and [Schema Contract](./schema-contract.md).
|
|
289
138
|
|
|
290
|
-
##
|
|
139
|
+
## What Ablo touches in your database — the honest footprint
|
|
291
140
|
|
|
292
|
-
|
|
293
|
-
`ablo_outbox` row per operation in the same transaction, and the adapter's
|
|
294
|
-
built-in events handler streams those rows back to Ablo by cursor — so connected
|
|
295
|
-
humans and agents stay current with no extra code. If Ablo already appended the
|
|
296
|
-
commit directly, `clientTxId` lets Ablo filter the echo; if the direct append
|
|
297
|
-
failed, the same outbox row repairs it on the next poll or push.
|
|
141
|
+
This is the complete list. Nothing else.
|
|
298
142
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
143
|
+
| Object | What it is | Owned by |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| `ablo_publication` | A Postgres publication naming the tables Ablo reads. | You create it (step 2). |
|
|
146
|
+
| Replication slot | A logical slot Ablo subscribes through to track its WAL position. | Ablo's runtime creates it on first connect. |
|
|
147
|
+
| `ablo_replicator` role | A least-privilege `REPLICATION` + `SELECT` role. | You create it (step 2). |
|
|
148
|
+
| `wal_level = logical` | A server setting that **requires a restart**. | You set it (step 1). |
|
|
149
|
+
|
|
150
|
+
Operational reality you should know up front:
|
|
151
|
+
|
|
152
|
+
- **`wal_level = logical` needs a restart.** It is a one-time, server-wide change
|
|
153
|
+
and is not reloadable.
|
|
154
|
+
- **A replication slot retains WAL.** While Ablo is connected, the slot holds the
|
|
155
|
+
WAL it hasn't yet acknowledged. If Ablo is disconnected for a long time, that
|
|
156
|
+
WAL accumulates and consumes disk. **Ablo monitors slot lag and WAL retention**
|
|
157
|
+
and surfaces it so you're never surprised by disk pressure; an abandoned slot is
|
|
158
|
+
dropped rather than left to grow unbounded.
|
|
159
|
+
- **The role is read-only.** It can stream replication and `SELECT`. It cannot
|
|
160
|
+
write, and the recipe never grants it more.
|
|
161
|
+
|
|
162
|
+
What Ablo explicitly does **not** do:
|
|
163
|
+
|
|
164
|
+
- It **never runs DDL** against your database.
|
|
165
|
+
- It **never owns or migrates your schema** — your migration tool stays in charge.
|
|
166
|
+
- It **never writes your rows** — writes are yours, through your backend.
|
|
167
|
+
|
|
168
|
+
## What Ablo stores on its side
|
|
169
|
+
|
|
170
|
+
Your schema *definition* (model names, fields, types — pushed with `ablo push`),
|
|
171
|
+
your hashed API keys, a safe projection of the connection registration (host,
|
|
172
|
+
database, schema — the connection string itself is sealed and never echoed back),
|
|
173
|
+
the replication slot position, and the commit log that drives sync. Never your
|
|
174
|
+
rows.
|
|
175
|
+
|
|
176
|
+
> **Logical-replication runtime status: Preview.** The setup path above
|
|
177
|
+
> (`ablo connect` and `ablo connect --check`) is real and shipping. The
|
|
178
|
+
> server-side component that consumes your WAL and turns it into sync deltas is in
|
|
179
|
+
> **Preview** — it is implemented and tested but **not yet GA / boot-wired in the
|
|
180
|
+
> hosted runtime**. Treat WAL consumption as not-yet-deployed until this note is
|
|
181
|
+
> removed. Maintainers: see
|
|
182
|
+
> [internal/byo-wal-consumer.md](./internal/byo-wal-consumer.md) for the
|
|
183
|
+
> architecture and remaining slices.
|
|
184
|
+
|
|
185
|
+
## Next steps
|
|
186
|
+
|
|
187
|
+
- [Quickstart](./quickstart.md) — connect and write through `ablo.<model>`.
|
|
188
|
+
- [Schema Contract](./schema-contract.md) — what the schema drives across SDK,
|
|
189
|
+
React, and agents.
|
|
190
|
+
- [Guarantees](./guarantees.md) — what confirmed writes and stale checks mean.
|
|
191
|
+
- [Integration Guide](./integration-guide.md) — the full app, React, multiplayer,
|
|
192
|
+
and agent path.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Legacy / not recommended
|
|
197
|
+
|
|
198
|
+
> **Use logical replication instead** (top of this page). The shapes below
|
|
199
|
+
> predate the single connect path. They are documented only so existing
|
|
200
|
+
> integrations can be read and understood — do **not** reach for them when
|
|
201
|
+
> connecting a new database. They are the seams that caused painful onboarding,
|
|
202
|
+
> and `ablo connect` exists precisely to replace them.
|
|
203
|
+
|
|
204
|
+
These older shapes connected Ablo to a database two other ways: by handing Ablo a
|
|
205
|
+
**connection string** to operate directly (`databaseUrl` on the client, committing
|
|
206
|
+
writes itself behind row-level security), or by exposing a **signed Data Source
|
|
207
|
+
endpoint** built from an ORM adapter (`prismaDataSource` / `drizzleDataSource`,
|
|
208
|
+
with `ablo_outbox` / `ablo_idempotency` bookkeeping and a reverse-channel
|
|
209
|
+
connector for VPCs). Both required Ablo to either operate your database or proxy
|
|
210
|
+
every write, and both have been superseded by logical replication, where Ablo only
|
|
211
|
+
reads your WAL. If you are maintaining one of these integrations, migrate it to
|
|
212
|
+
`ablo connect` at the next opportunity.
|