@abloatai/ablo 0.16.3 → 0.18.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.
@@ -1,335 +1,212 @@
1
1
  # Connect Your Database
2
2
 
3
- **In production, your database is the system of record.** Every synced model is
4
- backed by your own Postgres; Ablo is the transaction layer on top of it. There
5
- are two ways to connect, and they are the same product with the same writes — the
6
- only difference is where your database credential lives:
7
-
8
- | | How Ablo reaches your Postgres | Use when |
9
- |---|---|---|
10
- | **Connection string** (primary) | You pass `databaseUrl` to `Ablo(...)` explicitly (it is never auto-read from the environment); Ablo registers the connection and commits each write directly, behind row-level security. | You can hand over a scoped connection string. |
11
- | **Signed endpoint** | Your app exposes one route built from an ORM adapter; Ablo sends signed commit requests and your app writes its own database. | Database credentials must never leave your infrastructure. |
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
- > `databaseUrl`, like Stripe test mode. Connect your Postgres (either shape
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
- Either way, you define an Ablo schema with `defineSchema`, `model`, and Zod. The
19
- Ablo schema describes **only your synced, collaborative models** — the rows Ablo
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
- What Ablo stores, in both shapes: your schema *definition* (model names, fields,
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
- ## Connection String (default)
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
- The canonical client carries all three values:
25
+ ### 1. Enable logical decoding
36
26
 
37
- ```ts
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
- export const ablo = Ablo({
42
- schema,
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
- ```bash
49
- # .env server runtime only, never the browser
50
- DATABASE_URL=postgres://ablo_app:...@host:5432/db
51
- ABLO_API_KEY=sk_live_...
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
- - **Non-superuser role.** The connection must not be a superuser or hold
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
- `databaseUrl` is server-only: the SDK throws if it sees one in a browser-like
84
- environment, and `dangerouslyAllowBrowser` does not override that. It is also
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
- Multiplayer behavior is built in. Writes made through
111
- `ablo.<model>.create/update/delete` are coordinated by Ablo, then confirmed rows
112
- fan out to subscribers. If something writes to your database without going
113
- through Ablo (a cron job, an admin tool), Ablo can't know about it
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
- ## Your Database Stays Canonical
49
+ - **A publication** naming the tables Ablo should read (`ablo_publication`, the
50
+ single canonical name the runtime subscribes to):
119
51
 
120
- Your application database remains the source of truth and Ablo coordinates writes
121
- against it.
52
+ ```sql
53
+ CREATE PUBLICATION "ablo_publication" FOR ALL TABLES;
54
+ ```
122
55
 
123
- If you are migrating an app where every button already calls a backend endpoint,
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
- ## What Ablo Gives You
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
- When you add a Data Source in Ablo, you get:
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
- | Field | Purpose |
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 shape is the same as a production webhook integration:
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
- 1. Expose one Data Source endpoint in your app.
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
- // Data Source routes touch the database, so they run on the Node runtime.
162
- export const runtime = 'nodejs';
77
+ Put the replication role's connection string in `DATABASE_URL`, then verify the
78
+ database is replication-ready:
163
79
 
164
- export const { POST } = dataSourceNext({
165
- schema,
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
- Using Drizzle instead of Prisma is the same shape swap the adapter for
172
- `drizzleDataSource(db, schema)`:
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
- export const runtime = 'nodejs';
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
- export const { POST } = dataSourceNext({
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
- The adapter is constructed from your ORM client and the Ablo `schema` —
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
- Your app code still writes through the normal model API:
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
- ```ts
199
- await ablo.weatherReports.update({
200
- id: 'report_stockholm',
201
- data: { status: 'ready' },
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
- // scripts/ablo-connector.ts — run locally, or as a sidecar in a private VPC
223
- import { dataSource, createSourceConnector } from '@abloatai/ablo';
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 connector = createSourceConnector({
227
- apiKey: process.env.ABLO_API_KEY!, // sk_test_* for the dev loop
228
- handler: dataSource(sourceOptions), // the unchanged (Request) => Response
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
- When a connector is attached for a source, Ablo Cloud drains that source's
236
- `commit`/`load`/`list` down the socket instead of POSTing the webhook; when none
237
- is attached, the inbound webhook path is used unchanged. The drained requests
238
- carry the **same** Standard Webhooks signature, so `dataSource` verifies them
239
- exactly as on the webhook path the transport changes, the trust model does not.
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
- **Production / no-public-URL deploy.** By default the connector is gated to
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
- ## Commit Request
250
-
251
- When Ablo calls your Data Source, it sends a signed JSON request:
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
- type: 'commit',
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
- Use explicit `deltas` only when your source already computes canonical change
288
- events.
136
+ For the typed read/claim/write surface itself, see
137
+ [Quickstart](./quickstart.md) and [Schema Contract](./schema-contract.md).
289
138
 
290
- ## Outbox Events
139
+ ## What Ablo touches in your database — the honest footprint
291
140
 
292
- The adapter serves the outbox feed for you. Every `commit` it runs appends one
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
- Events without `clientTxId` are treated as external writes. The only thing you
300
- add by hand is recording *outside* writes — changes made to your tables by a
301
- cron job or admin tool that never went through Ablo. Append an `ablo_outbox` row
302
- (with no `clientTxId`) for those in the same transaction as the change, and the
303
- adapter's feed carries them to every connected screen.
304
-
305
- ## Production Checklist (signed endpoint)
306
-
307
- Before using the signed-endpoint shape in production:
308
-
309
- - Keep `DATABASE_URL` in the customer app or backend environment.
310
- - Use only the Data Source endpoint and `ABLO_API_KEY` as the customer-facing integration boundary.
311
- - Run the adapter migrations so `ablo_outbox` and `ablo_idempotency` exist
312
- alongside your synced tables (`ablo migrate`).
313
- - Set `export const runtime = 'nodejs'` on the route so it can reach the database.
314
- - For writes that bypass Ablo (cron, admin tools), append an `ablo_outbox` row
315
- (no `clientTxId`) in the same transaction as the change.
316
- - Monitor last success, last error, retry count, event lag, and cursor.
317
-
318
- The adapter already handles the rest — signature verification, the commit
319
- transaction, `clientTxId` idempotency, returning canonical rows, the outbox
320
- append per operation, and deduping the feed by event `id`. You don't write any of
321
- that by hand.
322
-
323
- In this shape, leave `databaseUrl` out of `Ablo(...)` — the endpoint *is* the
324
- connection, and registering both would point Ablo at your database twice.
325
-
326
- ## Security
327
-
328
- - Verify requests with `ABLO_API_KEY`.
329
- - Keep database credentials in your app.
330
- - Dedupe commits by `clientTxId`.
331
- - Dedupe external events by event `id`.
332
- - Use HTTPS in production.
333
-
334
- The API key is not a database credential. It only lets your route verify that
335
- the request came from Ablo and was not modified in transit.
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.