@abloatai/ablo 0.24.0 → 0.25.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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.25.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ca30064: Logical replication is now the documented default storage path, with self-service data-source registration from the CLI.
8
+ - **`ablo connect --register`** — registers your database as an Ablo data source over logical replication in one step: it runs the same pre-flight replication probe `ablo connect` uses (server reachable, `REPLICATION` privilege, `wal_level=logical`, publication/slot creatable), and on success `POST`s the connection to the engine's `/v1/datasources`. The `ek_`-authed call scopes the source to your org automatically; the password is stored decomposed as a secret, never echoed back. This is the "registration is the enable" path — there is no separate tier or flag to pick.
9
+ - **`ablo init` leads with logical replication** — the default storage mode is now `replication` (was `endpoint`); the generated env + next-steps point at `ablo connect` / `ablo connect --register`. The signed-endpoint and direct modes remain as the explicit fallback / legacy options.
10
+ - **`ablo status`** — a data-plane diagnostic that probes whether your registered source is reachable and replicating (failure-only reporting, so it never falsely reports healthy).
11
+ - **`ablo push`** — minor guard/UX refinements (deploy-target clarity).
12
+ - **Docs (README, `docs/data-sources.md`, `llms.txt`)** rewritten to the one-path model: Ablo consumes your database's logical-replication stream and your application owns the write path. The security wording is precise — a logical-replication connection requires the `REPLICATION` privilege (it is **not** a read-only SQL account), so reviews are not misled by a "read-only" claim.
13
+
14
+ No breaking changes to the SDK runtime API. The server-side WAL read cutover these CLI changes support ships by deploying the engine, not this package.
15
+
3
16
  ## 0.24.0
4
17
 
5
18
  ### Minor Changes
package/README.md CHANGED
@@ -55,11 +55,13 @@ claims are visible while the work is still in progress.
55
55
  [Version History & Migration Guide](./docs/migration.md)
56
56
 
57
57
  It works with the auth and database you already have. **In production, your
58
- database is the system of record.** Ablo is the transaction layer on top of it:
59
- realtime data is scoped to *sync groups* from your own identity, and every
60
- committed row lives in your Postgres. (Trying Ablo with no database yet? The
61
- hosted **sandbox** can host rows in Ablo's test plane apiKey only, like
62
- Stripe test mode so you can explore before pointing it at your Postgres.)
58
+ database is the system of record.** Ablo is the sync + coordination layer on top
59
+ of it: it consumes your Postgres' logical-replication stream, scopes realtime
60
+ data to *sync groups* from your own identity, and your application keeps owning
61
+ the write path every row lives in your Postgres. (Trying Ablo with no database
62
+ yet? A **sandbox** `sk_test` key holds throwaway **test data** like Stripe test
63
+ mode — so you can explore before pointing it at your Postgres. Test-mode only; in
64
+ production every row lives in your database.)
63
65
 
64
66
  **Built for** collaborative editors, AI agent workflows, and internal tools —
65
67
  anywhere people and agents change shared state and everyone has to see it live.
@@ -92,11 +94,11 @@ what's configured at any time.
92
94
 
93
95
  **Keys & runtime.** Ablo needs Node 24+ and TypeScript 5+. Keys come in two of
94
96
  *your* environments — `sk_test_` and `sk_live_`, like Stripe — and `ablo login`
95
- mints both. Keep the key and the database URL in trusted server runtimes only.
96
- In the browser, `<AbloProvider>` authenticates with the signed-in user's
97
- session never the raw key, never the database URL. Prefer the connection
98
- string never leaving your infrastructure? Expose a signed
99
- [Data Source endpoint](./docs/data-sources.md) instead and omit `databaseUrl`.
97
+ mints both. Keep the key in trusted server runtimes only. In the browser,
98
+ `<AbloProvider>` authenticates with the signed-in user's session — never the raw
99
+ key. Your database is connected once, out of band, via `npx ablo connect`
100
+ (logical replication); if it can't grant a replication role, expose a signed
101
+ [Data Source endpoint](./docs/data-sources.md) instead.
100
102
 
101
103
  For production (React, an existing backend, Data Source, agents), the
102
104
  [Integration Guide](./docs/integration-guide.md) is the deeper map.
@@ -175,8 +177,9 @@ const schema = defineSchema({
175
177
  const ablo = Ablo({
176
178
  schema,
177
179
  apiKey: process.env.ABLO_API_KEY, // written to .env.local by `npx ablo push`
178
- databaseUrl: process.env.DATABASE_URL, // your Postgres, passed explicitly — rows live here
179
180
  });
181
+ // Your Postgres is connected once via `npx ablo connect` (logical replication) —
182
+ // not passed here. Ablo tails its WAL; your rows never leave your database.
180
183
 
181
184
  await ablo.ready();
182
185
 
@@ -403,10 +406,12 @@ each other's changes in real time — that's the default, not a feature you turn
403
406
  - `useAblo(...)` gives React clients the live row, kept current automatically.
404
407
  - `ablo.<model>.claim({ id })` / `claim.state({ id })` / `claim.queue({ id })` let humans and agents coordinate (and observe) active work on a row — and the line waiting behind it — before a write lands.
405
408
 
406
- Always write through Ablo either the SDK model methods
407
- (`ablo.<model>.create/update/delete`) or the HTTP write endpoint below. If you
408
- write straight to your own database instead, those changes won't reach connected
409
- clients.
409
+ Your application owns the write path: writes that land in your database — through
410
+ your own backend, your existing API, any path — are tailed off the WAL and fanned
411
+ out to connected clients automatically (the Electric model). The SDK model
412
+ methods (`ablo.<model>.create/update/delete`) and the HTTP write endpoint are the
413
+ *JavaScript/agent* ergonomic surface — they participate in claims and confirmation
414
+ — but they are not a requirement for your normal writes to show up.
410
415
 
411
416
  ## HTTP Writes
412
417
 
@@ -429,35 +434,39 @@ curl https://api.abloatai.com/v1/commits \
429
434
 
430
435
  ## Your Database
431
436
 
432
- In production, every schema model is backed by **your own database** — Ablo is
433
- the transaction layer on top of it. Two ways to connect it:
437
+ In production, every schema model is backed by **your own database** — Ablo
438
+ **consumes** it, never owns it. The model is Electric's (and PowerSync's, and
439
+ Zero's): Ablo consumes your Postgres' logical-replication stream and fans the
440
+ changes out; **your application continues to own the write path.** Two ways Ablo
441
+ connects:
434
442
 
435
- | | How Ablo reaches your Postgres | Use when |
443
+ | | How Ablo connects to your Postgres | Use when |
436
444
  | --- | --- | --- |
437
- | **Connection string** (primary) | `databaseUrl` at init passed explicitly, never auto-read from the environment. Ablo registers the connection once (sent over TLS, stored sealed, never echoed back) and commits each write directly through a non-superuser role, behind row-level security. | You can hand over a scoped connection string. |
438
- | **Signed endpoint** | Your app exposes one route built from an ORM adapter (`prismaDataSource` / `drizzleDataSource`); Ablo sends signed commit requests and your app writes its own database. | Database credentials must never leave your infrastructure. |
445
+ | **Logical replication** (primary) | `npx ablo connect` prints the setup SQL (`wal_level=logical`, a publication, a `REPLICATION` role); `npx ablo connect --register` tells Ablo to replicate it. Ablo tails the WAL it never runs DDL on, writes to, owns, or migrates your database. | Your database can grant a `REPLICATION` role (most can). |
446
+ | **Signed endpoint** (fallback) | Your app exposes one route built from an ORM adapter (`prismaDataSource` / `drizzleDataSource`); Ablo sends signed requests and your app touches its own database. Needs no database configuration. | Your database **can't** grant a replication role (a locked-down managed DB). |
439
447
 
440
- (No database yet? The hosted **sandbox** can host rows in Ablo's test plane
441
- omit `databaseUrl` and pass an `apiKey` only, like Stripe test mode — so you can
442
- try Ablo before connecting your Postgres.)
448
+ (No database yet? A **sandbox** `sk_test` key lets you try Ablo's full API and
449
+ coordination with throwaway **test data** like Stripe test mode — before you
450
+ connect your own Postgres. Test-mode only: in production your rows always live in
451
+ your database and Ablo holds just the transaction log, never your data.)
443
452
 
444
- Same product, same truth either way: in production your database is the system of
445
- record. See
446
- [Connect Your Database](./docs/data-sources.md) for both shapes.
453
+ Your database is the system of record. The old `databaseUrl` dial-in (Ablo
454
+ holding a read/write connection) is **deprecated and being removed** — connect
455
+ via logical replication instead. See
456
+ [Connect Your Database](./docs/data-sources.md).
447
457
 
448
458
  ## Configuration
449
459
 
450
- `Ablo({ ... })` takes your schema, your key, and in production — your database,
451
- either as an explicit `databaseUrl` here or as a signed
452
- [Data Source endpoint](./docs/data-sources.md) in your app. (`databaseUrl` is
453
- never auto-read from the environment; omit it to try Ablo against the hosted
454
- sandbox.) Every other option has correct defaults:
460
+ `Ablo({ ... })` takes your schema and your key. Your database is connected
461
+ **out of band** once, via `npx ablo connect` (logical replication) or a signed
462
+ [Data Source endpoint](./docs/data-sources.md) not through the constructor.
463
+ Every other option has correct defaults:
455
464
 
456
465
  | Option | Type | Default | Purpose |
457
466
  | --- | --- | --- | --- |
458
467
  | `schema` | `Schema` | — (required) | Typed model proxies (`ablo.<model>.*`) |
459
468
  | `apiKey` | `string \| ApiKeySetter \| null` | `process.env.ABLO_API_KEY` | Server key — a string, or an async function for rotation |
460
- | `databaseUrl` | `string \| null` | `—` | Your Postgres, registered as the data plane. **Must be passed explicitly it is not auto-read from the environment.** If you have a `DATABASE_URL` set for another tool (Prisma, Drizzle, docker-compose), `Ablo()` ignores it unless you pass `databaseUrl` explicitly. Server runtimes only the SDK throws if it sees this in a browser. Omit it when your app exposes a signed [Data Source endpoint](./docs/data-sources.md) instead, or when trying Ablo against the hosted sandbox. |
469
+ | `databaseUrl` | `string \| null` | `—` | **Deprecated being removed.** The old dial-in (Ablo holding a read/write connection to your DB). Connect via `npx ablo connect` (logical replication) instead; Ablo tails your WAL rather than dialing in. See [Connect Your Database](./docs/data-sources.md). |
461
470
 
462
471
  Keep `apiKey` in trusted server runtimes. In the browser, `<AbloProvider>`
463
472
  authenticates with the signed-in user's session; the raw-key path is gated
@@ -512,7 +521,7 @@ contract; there are no retry or timeout knobs to tune.
512
521
  - [React](./docs/react.md) — `<AbloProvider>`, `useAblo`, presence, status, and bootstrap gating.
513
522
  - [Coordination](./docs/coordination.md) — `claim` / `claim.state` / `claim.queue` / `claim.release` reference: hold a row across slow agent work, and observe the line waiting behind it.
514
523
  - [Client Behavior](./docs/client-behavior.md) — options, errors, retries, timeouts, and public imports.
515
- - [Connect Your Database](./docs/data-sources.md) — connect your Postgres by connection string (`databaseUrl`) or signed endpoint; your database is the system of record either way.
524
+ - [Connect Your Database](./docs/data-sources.md) — connect your Postgres by logical replication (`npx ablo connect`) or, as a fallback, a signed endpoint; your database is the system of record either way.
516
525
  - [Existing Python Backend](./docs/examples/existing-python-backend.md) — migrate existing Python endpoints to multiplayer and agent-safe writes gradually.
517
526
  - [AI SDK Tool](./docs/examples/ai-sdk-tool.md) — use Ablo inside an AI SDK tool call.
518
527
  - [Server Agent](./docs/examples/server-agent.md) — schema-backed worker.
package/dist/cli.cjs CHANGED
@@ -212120,17 +212120,19 @@ var require_commonjs2 = __commonJS({
212120
212120
  }
212121
212121
  function expand_(str, max, isTop) {
212122
212122
  const expansions = [];
212123
- const m2 = (0, balanced_match_1.balanced)("{", "}", str);
212124
- if (!m2)
212125
- return [str];
212126
- const pre = m2.pre;
212127
- const post = m2.post.length ? expand_(m2.post, max, false) : [""];
212128
- if (/\$$/.test(m2.pre)) {
212129
- for (let k3 = 0; k3 < post.length && k3 < max; k3++) {
212130
- const expansion = pre + "{" + m2.body + "}" + post[k3];
212131
- expansions.push(expansion);
212123
+ for (; ; ) {
212124
+ const m2 = (0, balanced_match_1.balanced)("{", "}", str);
212125
+ if (!m2)
212126
+ return [str];
212127
+ const pre = m2.pre;
212128
+ if (/\$$/.test(m2.pre)) {
212129
+ const post2 = m2.post.length ? expand_(m2.post, max, false) : [""];
212130
+ for (let k3 = 0; k3 < post2.length && k3 < max; k3++) {
212131
+ const expansion = pre + "{" + m2.body + "}" + post2[k3];
212132
+ expansions.push(expansion);
212133
+ }
212134
+ return expansions;
212132
212135
  }
212133
- } else {
212134
212136
  const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m2.body);
212135
212137
  const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m2.body);
212136
212138
  const isSequence = isNumericSequence || isAlphaSequence;
@@ -212138,10 +212140,12 @@ var require_commonjs2 = __commonJS({
212138
212140
  if (!isSequence && !isOptions) {
212139
212141
  if (m2.post.match(/,(?!,).*\}/)) {
212140
212142
  str = m2.pre + "{" + m2.body + escClose + m2.post;
212141
- return expand_(str, max, true);
212143
+ isTop = true;
212144
+ continue;
212142
212145
  }
212143
212146
  return [str];
212144
212147
  }
212148
+ const post = m2.post.length ? expand_(m2.post, max, false) : [""];
212145
212149
  let n;
212146
212150
  if (isSequence) {
212147
212151
  n = m2.body.split(/\.\./);
@@ -212205,8 +212209,8 @@ var require_commonjs2 = __commonJS({
212205
212209
  }
212206
212210
  }
212207
212211
  }
212212
+ return expansions;
212208
212213
  }
212209
- return expansions;
212210
212214
  }
212211
212215
  }
212212
212216
  });
@@ -280268,6 +280272,12 @@ async function push(argv) {
280268
280272
  ` One or more synced tables don't have FORCE ROW LEVEL SECURITY. Run ${import_picocolors3.default.bold("npx ablo migrate")} to (re)apply the tenant policies, then re-push.`
280269
280273
  )
280270
280274
  );
280275
+ } else if (code === "capability_scope_denied" || code === "capability_invalid") {
280276
+ console.error(
280277
+ import_picocolors3.default.dim(
280278
+ ` This is a ${import_picocolors3.default.bold("database privilege")} error (Postgres 42501 / row-level security), not a key scope \u2014 a different API key won't help. The role behind this org's database can't write the target. Provision a writable, RLS-scoped role with ${import_picocolors3.default.bold("npx ablo migrate")}, or check the org's database registration. See docs/plans/read-path-logical-replication-vs-hosting.md.`
280279
+ )
280280
+ );
280271
280281
  } else if (args.apiKey != null && classifyCredentialKind(args.apiKey) === "restricted") {
280272
280282
  console.error(
280273
280283
  import_picocolors3.default.dim(
@@ -280464,6 +280474,8 @@ var ABLO_PUBLICATION = "ablo_publication";
280464
280474
  var ABLO_REPLICATION_ROLE = "ablo_replicator";
280465
280475
  function parseConnectArgs(argv) {
280466
280476
  let check2 = false;
280477
+ let register = false;
280478
+ let auditInfra = false;
280467
280479
  let tables = [];
280468
280480
  let role = ABLO_REPLICATION_ROLE;
280469
280481
  for (let i = 0; i < argv.length; i++) {
@@ -280472,6 +280484,12 @@ function parseConnectArgs(argv) {
280472
280484
  case "--check":
280473
280485
  check2 = true;
280474
280486
  break;
280487
+ case "--register":
280488
+ register = true;
280489
+ break;
280490
+ case "--audit-infra":
280491
+ auditInfra = true;
280492
+ break;
280475
280493
  case "--tables": {
280476
280494
  const value = argv[++i] ?? "";
280477
280495
  tables = value.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
@@ -280484,7 +280502,7 @@ function parseConnectArgs(argv) {
280484
280502
  throw new AbloValidationError(`unknown flag: ${arg}`, { code: "cli_invalid_arguments" });
280485
280503
  }
280486
280504
  }
280487
- return { check: check2, tables, role };
280505
+ return { check: check2, register, auditInfra, tables, role };
280488
280506
  }
280489
280507
  function quoteIdent(id) {
280490
280508
  return `"${id.replace(/"/g, '""')}"`;
@@ -280508,12 +280526,12 @@ function connectSetupSql(input) {
280508
280526
  function printConnectRecipe(args) {
280509
280527
  const sql = connectSetupSql({ tables: args.tables, role: args.role });
280510
280528
  console.log(`
280511
- ${brand("ablo")} ${import_picocolors5.default.dim("connect")} ${import_picocolors5.default.dim("logical replication \u2014 the one way to connect a real database")}
280529
+ ${brand("ablo")} ${import_picocolors5.default.dim("connect")} ${import_picocolors5.default.dim("logical replication \u2014 the read path (your writes stay on your own backend)")}
280512
280530
  `);
280513
280531
  console.log(
280514
- ` Ablo READS your write-ahead log (WAL) and ${import_picocolors5.default.bold("never")} runs DDL, owns, or migrates your
280515
- schema. Your app keeps writing through your own backend \u2014 Ablo only tails the changes.
280516
- Run this once against your Postgres ${import_picocolors5.default.dim("(as a superuser / the DB owner)")}:
280532
+ ` Ablo consumes your write-ahead log (WAL) via logical replication and ${import_picocolors5.default.bold("never")} runs DDL,
280533
+ owns, hosts, or migrates your schema. Your app continues to own the write path \u2014 Ablo tails
280534
+ the changes and serves them as live shapes. Run this once against your Postgres ${import_picocolors5.default.dim("(as a superuser / DB owner)")}:
280517
280535
  `
280518
280536
  );
280519
280537
  console.log(` ${import_picocolors5.default.bold("1.")} Enable logical decoding ${import_picocolors5.default.dim("(then RESTART Postgres \u2014 wal_level is not reloadable)")}`);
@@ -280549,12 +280567,24 @@ function printConnectRecipe(args) {
280549
280567
  );
280550
280568
  console.log(
280551
280569
  import_picocolors5.default.dim(
280552
- ` Reminder: Ablo never writes to your database on this path. Provisioning tables with
280553
- ${import_picocolors5.default.bold("ablo migrate")} is a separate, optional escape hatch \u2014 connecting a real database is this.`
280570
+ ` Reminder: this is the READ path \u2014 Ablo tails your WAL and never writes to your database.
280571
+ Your writes keep going through your own backend. (Ablo HOSTING your rows, or dialing in via
280572
+ ${import_picocolors5.default.bold("databaseUrl")}, is the deprecated posture \u2014 see the read-path decision doc.)`
280554
280573
  )
280555
280574
  );
280556
280575
  console.log();
280557
280576
  }
280577
+ var SYNC_INFRA_RELATIONS = [
280578
+ "sync_deltas",
280579
+ "sync_id_seq",
280580
+ "sync_metadata",
280581
+ "mutation_log"
280582
+ ];
280583
+ var SYNC_INFRA_TYPES = [
280584
+ "participant_kind",
280585
+ "backfill_provenance",
280586
+ "confirmation_state"
280587
+ ];
280558
280588
  function printCheckItem(item) {
280559
280589
  if (item.ok) {
280560
280590
  console.log(` ${import_picocolors5.default.green("\u2713")} ${item.label}`);
@@ -280568,14 +280598,17 @@ function printCheckItem(item) {
280568
280598
  async function probeReadiness(sql, opts = {}) {
280569
280599
  const publication = opts.publication ?? ABLO_PUBLICATION;
280570
280600
  const items = [];
280571
- const walRows = await sql.unsafe(`SHOW wal_level`);
280601
+ const walRows = await sql.unsafe(
280602
+ `SELECT setting FROM pg_settings WHERE name = 'wal_level'`
280603
+ );
280572
280604
  const walLevel = walRows[0]?.setting ?? "unknown";
280573
280605
  items.push(
280574
280606
  walLevel === "logical" ? { ok: true, label: `wal_level is ${import_picocolors5.default.bold("logical")}` } : {
280575
280607
  ok: false,
280576
280608
  label: `wal_level is ${import_picocolors5.default.bold(walLevel)} (need ${import_picocolors5.default.bold("logical")})`,
280577
280609
  fix: `ALTER SYSTEM SET wal_level = 'logical'; then RESTART Postgres.
280578
- On RDS/Aurora set rds.logical_replication = 1 in the parameter group, then reboot.`
280610
+ On RDS/Aurora set rds.logical_replication = 1 in the parameter group, then reboot.
280611
+ On Neon enable Logical Replication in the project (Console \u2192 Settings \u2192 Logical Replication, or the API) \u2014 Neon forbids ALTER SYSTEM; the toggle sets wal_level=logical.`
280579
280612
  }
280580
280613
  );
280581
280614
  const pubRows = await sql.unsafe(
@@ -280636,17 +280669,35 @@ On RDS: GRANT rds_replication TO <your_role>;`
280636
280669
  }
280637
280670
  return items;
280638
280671
  }
280639
- async function runCheck() {
280672
+ async function auditTenantSyncInfra(sql) {
280673
+ const artifacts = [];
280674
+ for (const name of SYNC_INFRA_RELATIONS) {
280675
+ const rows = await sql.unsafe(
280676
+ `SELECT to_regclass($1)::text AS reg`,
280677
+ [`public.${name}`]
280678
+ );
280679
+ artifacts.push({ kind: "relation", name, present: rows[0]?.reg != null });
280680
+ }
280681
+ for (const name of SYNC_INFRA_TYPES) {
280682
+ const rows = await sql.unsafe(
280683
+ `SELECT to_regtype($1)::text AS reg`,
280684
+ [`public.${name}`]
280685
+ );
280686
+ artifacts.push({ kind: "type", name, present: rows[0]?.reg != null });
280687
+ }
280688
+ return artifacts;
280689
+ }
280690
+ function requireDatabaseUrl(verb) {
280640
280691
  const dbUrl = readProjectDatabaseUrl();
280641
280692
  if (!dbUrl) {
280642
280693
  console.error(
280643
- import_picocolors5.default.red(" No DATABASE_URL found (checked process env, .env.local, .env).") + import_picocolors5.default.dim(` Set it to the Postgres you want Ablo to read, then re-run ${import_picocolors5.default.bold("ablo connect --check")}.`)
280694
+ import_picocolors5.default.red(" No DATABASE_URL found (checked process env, .env.local, .env).") + import_picocolors5.default.dim(` Set it to the Postgres you want Ablo to read, then re-run ${import_picocolors5.default.bold(`ablo connect ${verb}`)}.`)
280644
280695
  );
280645
280696
  process.exit(1);
280646
280697
  }
280647
- console.log(`
280648
- ${brand("ablo")} ${import_picocolors5.default.dim("connect --check")} ${import_picocolors5.default.dim("logical-replication readiness")}
280649
- `);
280698
+ return dbUrl;
280699
+ }
280700
+ async function probeAndReport(dbUrl) {
280650
280701
  const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
280651
280702
  } });
280652
280703
  let items;
@@ -280660,7 +280711,14 @@ async function runCheck() {
280660
280711
  }
280661
280712
  await sql.end({ timeout: 2 });
280662
280713
  for (const item of items) printCheckItem(item);
280663
- const failures = items.filter((i) => !i.ok).length;
280714
+ return items.filter((i) => !i.ok).length;
280715
+ }
280716
+ async function runCheck() {
280717
+ const dbUrl = requireDatabaseUrl("--check");
280718
+ console.log(`
280719
+ ${brand("ablo")} ${import_picocolors5.default.dim("connect --check")} ${import_picocolors5.default.dim("logical-replication readiness")}
280720
+ `);
280721
+ const failures = await probeAndReport(dbUrl);
280664
280722
  console.log();
280665
280723
  if (failures === 0) {
280666
280724
  console.log(` ${import_picocolors5.default.green("\u2713")} Ready \u2014 Ablo can connect and tail this database's WAL.
@@ -280673,6 +280731,104 @@ async function runCheck() {
280673
280731
  );
280674
280732
  process.exit(1);
280675
280733
  }
280734
+ async function runRegister() {
280735
+ const dbUrl = requireDatabaseUrl("--register");
280736
+ const apiKey = resolveApiKey();
280737
+ if (!apiKey) {
280738
+ console.error(
280739
+ import_picocolors5.default.red(" Not logged in.") + import_picocolors5.default.dim(` Run ${import_picocolors5.default.bold("ablo login")} (or set ${import_picocolors5.default.bold("ABLO_API_KEY")}) so Ablo knows which project to register this database for.`)
280740
+ );
280741
+ process.exit(1);
280742
+ }
280743
+ console.log(`
280744
+ ${brand("ablo")} ${import_picocolors5.default.dim("connect --register")} ${import_picocolors5.default.dim("register this database for replication")}
280745
+ `);
280746
+ const failures = await probeAndReport(dbUrl);
280747
+ if (failures > 0) {
280748
+ console.log(
280749
+ `
280750
+ ${import_picocolors5.default.red(`${failures} item${failures === 1 ? "" : "s"} to fix`)} ${import_picocolors5.default.dim("\u2014 a database that isn\u2019t replication-ready can\u2019t stream. Fix the above, then re-run.")}
280751
+ `
280752
+ );
280753
+ process.exit(1);
280754
+ }
280755
+ const apiUrl2 = (process.env.ABLO_API_URL ?? DEFAULT_URL).replace(/\/+$/, "");
280756
+ let res;
280757
+ try {
280758
+ res = await fetch(`${apiUrl2}/v1/datasources`, {
280759
+ method: "POST",
280760
+ headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
280761
+ body: JSON.stringify({ connectionString: dbUrl })
280762
+ });
280763
+ } catch (err) {
280764
+ console.error(import_picocolors5.default.red(`
280765
+ Couldn't reach ${apiUrl2}: ${err instanceof Error ? err.message : String(err)}
280766
+ `));
280767
+ process.exit(1);
280768
+ }
280769
+ if (res.ok) {
280770
+ const body2 = await res.json().catch(() => ({}));
280771
+ console.log(
280772
+ `
280773
+ ${import_picocolors5.default.green("\u2713")} Registered${body2.host ? ` ${import_picocolors5.default.dim(body2.host)}` : ""}${body2.id ? ` ${import_picocolors5.default.dim(`(${body2.id})`)}` : ""} \u2014 Ablo will replicate this database on the next sync.
280774
+ `
280775
+ );
280776
+ process.exit(0);
280777
+ }
280778
+ const body = await res.json().catch(() => ({}));
280779
+ const code = body.error?.code;
280780
+ const message = body.error?.message ?? `HTTP ${res.status}`;
280781
+ console.error(import_picocolors5.default.red(`
280782
+ Registration failed: ${message}`));
280783
+ if (code === "forbidden") {
280784
+ console.error(import_picocolors5.default.dim(` Registering a database needs a ${import_picocolors5.default.bold("secret")} key (sk_\u2026). Run ${import_picocolors5.default.bold("ablo login")} for one.`));
280785
+ } else if (code === "datasource_direct_deprecated") {
280786
+ console.error(
280787
+ import_picocolors5.default.dim(
280788
+ ` This deployment doesn\u2019t accept new connection-string registrations yet. The operator must enable it (${import_picocolors5.default.bold("ABLO_ALLOW_DIRECT_DATASOURCE=true")}) \u2014 see ${import_picocolors5.default.bold("docs/runbooks/connect-customer-database-byo-replication.md")}.`
280789
+ )
280790
+ );
280791
+ } else if (code === "datasource_connection_unsupported") {
280792
+ console.error(
280793
+ import_picocolors5.default.dim(` This deployment can\u2019t accept connection strings \u2014 use a self-hosted/hosted engine, or the signed endpoint fallback.`)
280794
+ );
280795
+ }
280796
+ console.error();
280797
+ process.exit(1);
280798
+ }
280799
+ async function runAuditInfra() {
280800
+ const dbUrl = requireDatabaseUrl("--audit-infra");
280801
+ const sql = src_default(dbUrl, { max: 1, prepare: false, onnotice: () => {
280802
+ } });
280803
+ let artifacts;
280804
+ try {
280805
+ artifacts = await auditTenantSyncInfra(sql);
280806
+ } catch (err) {
280807
+ const pg = err ?? {};
280808
+ console.error(import_picocolors5.default.red(` Couldn't audit the database: ${pg.message ?? String(err)}`));
280809
+ await sql.end({ timeout: 2 });
280810
+ process.exit(1);
280811
+ }
280812
+ await sql.end({ timeout: 2 });
280813
+ console.log(`
280814
+ ${brand("ablo")} ${import_picocolors5.default.dim("connect --audit-infra")} ${import_picocolors5.default.dim("Stage 5 tenant DB sync-infra audit")}
280815
+ `);
280816
+ const present = artifacts.filter((a) => a.present);
280817
+ if (present.length === 0) {
280818
+ console.log(` ${import_picocolors5.default.green("\u2713")} No deprecated Ablo sync infrastructure found in public.
280819
+ `);
280820
+ process.exit(0);
280821
+ }
280822
+ for (const artifact of present) {
280823
+ const label = artifact.kind === "type" ? "type" : "relation";
280824
+ console.log(` ${import_picocolors5.default.yellow("!")} ${label} ${import_picocolors5.default.bold(`public.${artifact.name}`)} exists`);
280825
+ }
280826
+ console.log(
280827
+ `
280828
+ ${import_picocolors5.default.yellow(`${present.length} artifact${present.length === 1 ? "" : "s"} found`)} ` + import_picocolors5.default.dim("\u2014 do not drop automatically. Confirm the org/environment is log-authoritative, then follow ") + import_picocolors5.default.bold("docs/runbooks/wal-stage5-customer-db-infra-cleanup.md") + import_picocolors5.default.dim(".\n")
280829
+ );
280830
+ process.exit(1);
280831
+ }
280676
280832
  async function connect(argv) {
280677
280833
  let args;
280678
280834
  try {
@@ -280685,18 +280841,28 @@ async function connect(argv) {
280685
280841
  await runCheck();
280686
280842
  return;
280687
280843
  }
280844
+ if (args.register) {
280845
+ await runRegister();
280846
+ return;
280847
+ }
280848
+ if (args.auditInfra) {
280849
+ await runAuditInfra();
280850
+ return;
280851
+ }
280688
280852
  printConnectRecipe(args);
280689
280853
  }
280690
- var CONNECT_USAGE = ` ablo connect \u2014 connect a real database to Ablo via logical replication (the one way)
280854
+ var CONNECT_USAGE = ` ablo connect \u2014 connect your database for the read path, via logical replication
280691
280855
 
280692
- Ablo READS your WAL and never runs DDL, owns, or migrates your schema. Your app
280693
- keeps writing through your own backend.
280856
+ The Electric/PowerSync/Zero model: Ablo consumes your WAL via logical replication and
280857
+ never runs DDL, owns, hosts, or migrates your schema. Your app continues to own the write path.
280694
280858
 
280695
280859
  Usage:
280696
280860
  npx ablo connect Print the exact setup SQL for your Postgres
280697
280861
  npx ablo connect --tables a,b,c Publish only these tables (default: all tables)
280698
280862
  npx ablo connect --role <name> Name the replication role (default: ablo_replicator)
280699
- npx ablo connect --check Validate DATABASE_URL is replication-ready`;
280863
+ npx ablo connect --check Validate DATABASE_URL is replication-ready
280864
+ npx ablo connect --register Register DATABASE_URL so Ablo replicates it (one self-service step)
280865
+ npx ablo connect --audit-infra Read-only Stage 5 audit for deprecated Ablo sync tables/types`;
280700
280866
 
280701
280867
  // src/cli/generate.ts
280702
280868
  init_cjs_shims();
@@ -281445,6 +281611,45 @@ async function fetchPushedSchema(apiUrl2, apiKey) {
281445
281611
  clearTimeout(t);
281446
281612
  }
281447
281613
  }
281614
+ async function sampleRead(apiUrl2, apiKey, modelTypename, n) {
281615
+ const ctrl = new AbortController();
281616
+ const t = setTimeout(() => ctrl.abort(), 4e3);
281617
+ try {
281618
+ const res = await fetch(
281619
+ `${apiUrl2}/v1/models/${encodeURIComponent(modelTypename)}/__ablo_health_probe_${n}__`,
281620
+ { headers: { authorization: `Bearer ${apiKey}` }, signal: ctrl.signal }
281621
+ );
281622
+ if (res.ok) return "routed";
281623
+ let code;
281624
+ try {
281625
+ const body = await res.json();
281626
+ code = body.code ?? body.error?.code;
281627
+ } catch {
281628
+ }
281629
+ if (code === "entity_not_found") return "routed";
281630
+ if (code === "tenant_routing_failed") return "no_route";
281631
+ if (res.status === 401 || res.status === 403) return { forbidden: code };
281632
+ return { other: `${res.status}${code ? ` ${code}` : ""}` };
281633
+ } catch {
281634
+ return { other: "unreachable" };
281635
+ } finally {
281636
+ clearTimeout(t);
281637
+ }
281638
+ }
281639
+ async function probeDataPlane(apiUrl2, apiKey, modelTypename) {
281640
+ if (!apiKey) return { status: "skipped" };
281641
+ const samples = [];
281642
+ for (let i = 0; i < 3; i++) samples.push(await sampleRead(apiUrl2, apiKey, modelTypename, i));
281643
+ const forbidden = samples.find((s) => typeof s === "object" && "forbidden" in s);
281644
+ if (forbidden) return { status: "forbidden", detail: forbidden.forbidden };
281645
+ const routed = samples.filter((s) => s === "routed").length;
281646
+ const noRoute = samples.filter((s) => s === "no_route").length;
281647
+ if (routed === samples.length) return { status: "ok" };
281648
+ if (noRoute === samples.length) return { status: "no_database" };
281649
+ if (routed > 0 && noRoute > 0) return { status: "intermittent", ok: routed, failed: noRoute };
281650
+ const other = samples.find((s) => typeof s === "object" && "other" in s);
281651
+ return { status: "unknown", detail: other?.other ?? "inconclusive" };
281652
+ }
281448
281653
  function formatConflict(conflict) {
281449
281654
  if (!conflict) return "";
281450
281655
  const parts = ["user", "agent", "system"].flatMap((k3) => conflict[k3] ? [`${k3}:${conflict[k3]}`] : []);
@@ -281550,6 +281755,28 @@ async function status(args = []) {
281550
281755
  } else if (pushed && !pushed.active) {
281551
281756
  console.log(` ${import_picocolors11.default.dim("schema")} ${import_picocolors11.default.yellow("none pushed")} ${import_picocolors11.default.dim(`(run ${import_picocolors11.default.bold("ablo push")} or ${import_picocolors11.default.bold("ablo dev")})`)}`);
281552
281757
  }
281758
+ if (pushed && pushed.active && pushed.models.length > 0) {
281759
+ const probe = await probeDataPlane(apiUrl2, introspectKey, pushed.models[0].typename);
281760
+ if (probe.status === "no_database") {
281761
+ console.log(` ${import_picocolors11.default.dim("data")} ${import_picocolors11.default.red("\u2717 no database registered")}${org ? import_picocolors11.default.dim(` for org ${org}`) : ""}`);
281762
+ console.log(
281763
+ ` ${import_picocolors11.default.dim(
281764
+ `reads/writes will fail with ${import_picocolors11.default.bold("tenant_routing_failed")}. Connect one with ${import_picocolors11.default.bold("ablo connect")}, or point ${import_picocolors11.default.bold("ABLO_API_KEY")} at an org that has a database.`
281765
+ )}`
281766
+ );
281767
+ } else if (probe.status === "intermittent") {
281768
+ console.log(` ${import_picocolors11.default.dim("data")} ${import_picocolors11.default.red(`\u2717 database routing is intermittent`)} ${import_picocolors11.default.dim(`(${probe.ok} ok / ${probe.failed} failed of ${probe.ok + probe.failed})`)}${org ? import_picocolors11.default.dim(` for org ${org}`) : ""}`);
281769
+ console.log(
281770
+ ` ${import_picocolors11.default.dim(
281771
+ `some reads/writes fail with ${import_picocolors11.default.bold("tenant_routing_failed")} \u2014 the registration is unstable. Re-establish it with ${import_picocolors11.default.bold("ablo connect")} (or check for a recent server redeploy).`
281772
+ )}`
281773
+ );
281774
+ } else if (probe.status === "forbidden") {
281775
+ console.log(` ${import_picocolors11.default.dim("data")} ${import_picocolors11.default.yellow("? key not authorized to read")}${probe.detail ? import_picocolors11.default.dim(` (${probe.detail})`) : ""}`);
281776
+ } else if (probe.status === "unknown") {
281777
+ console.log(` ${import_picocolors11.default.dim("data")} ${import_picocolors11.default.yellow(`? data-plane check inconclusive (${probe.detail})`)}`);
281778
+ }
281779
+ }
281553
281780
  }
281554
281781
  console.log();
281555
281782
  }
@@ -283055,6 +283282,7 @@ async function main() {
283055
283282
  console.log(` npx ablo check Check your existing database fits the schema (read-only, creates no tables)`);
283056
283283
  console.log(` npx ablo connect Connect a real database \u2014 prints the logical-replication setup SQL (the one way)`);
283057
283284
  console.log(` npx ablo connect --check Validate DATABASE_URL is replication-ready (wal_level, publication, role, replica identity)`);
283285
+ console.log(` npx ablo connect --audit-infra Read-only audit for deprecated Ablo sync infra in a customer DB`);
283058
283286
  console.log(` npx ablo migrate Provision your synced-model tables in your own Postgres (optional escape hatch \u2014 \`connect\` is the way)`);
283059
283287
  console.log(` npx ablo migrate --dry-run Print the SQL without executing (preview)`);
283060
283288
  console.log(` npx ablo push Upload your schema definition to Ablo (metadata only \u2014 rows stay in your DB)`);
@@ -283080,7 +283308,7 @@ function bailIfCancelled(value) {
283080
283308
  }
283081
283309
  var INIT_FRAMEWORKS = ["nextjs", "vite", "remix", "vanilla"];
283082
283310
  var INIT_AUTHS = ["apikey", "firebase", "auth0", "clerk", "supabase", "betterauth", "jwt"];
283083
- var INIT_STORAGES = ["endpoint", "direct", "datasource"];
283311
+ var INIT_STORAGES = ["replication", "endpoint", "direct", "datasource"];
283084
283312
  function parseInitArgs(args) {
283085
283313
  const has = (flag2) => args.includes(flag2);
283086
283314
  const val = (flag2) => {
@@ -283206,15 +283434,15 @@ async function init(args = []) {
283206
283434
  const storageChoice = await chooseOption(
283207
283435
  "storage",
283208
283436
  opts.storage,
283209
- "endpoint",
283437
+ "replication",
283210
283438
  INIT_STORAGES,
283211
283439
  false,
283212
- () => Promise.resolve("endpoint")
283440
+ () => Promise.resolve("replication")
283213
283441
  );
283214
283442
  const storage = storageChoice === "datasource" ? "endpoint" : storageChoice;
283215
283443
  if (storage === "direct") {
283216
283444
  Me(
283217
- "`--storage direct` uses the deprecated databaseUrl connector. The signed Data\nSource endpoint is the supported path.",
283445
+ "`--storage direct` uses the deprecated databaseUrl connector. Logical replication\n(`npx ablo connect`) is the supported path; a signed Data Source endpoint is the fallback.",
283218
283446
  import_picocolors20.default.yellow("Deprecated")
283219
283447
  );
283220
283448
  }
@@ -283335,7 +283563,9 @@ async function init(args = []) {
283335
283563
  `Run ${import_picocolors20.default.bold("npx ablo login")} (or add ${import_picocolors20.default.bold("ABLO_API_KEY")} to ${import_picocolors20.default.bold(envFile)})`,
283336
283564
  `Set ${import_picocolors20.default.bold("DATABASE_URL")} in ${import_picocolors20.default.bold(envFile)} \u2014 your Postgres is the system of record; rows live there, never with Ablo`,
283337
283565
  `Run ${import_picocolors20.default.bold("npx ablo dev")} \u2014 pushes your schema definition and watches for changes`,
283338
- ...storage === "direct" ? [
283566
+ ...storage === "replication" ? [
283567
+ `Connect your database: ${import_picocolors20.default.bold("npx ablo connect")} prints the logical-replication setup SQL (run it once on your Postgres), then ${import_picocolors20.default.bold("npx ablo connect --register")} tells Ablo to replicate it \u2014 your app keeps writing through your own backend, Ablo tails the WAL`
283568
+ ] : storage === "direct" ? [
283339
283569
  `Provision your DB: ${import_picocolors20.default.bold("npx ablo migrate")} (creates your synced-model tables with row-level security; keep your own migrations for everything else)`
283340
283570
  ] : [
283341
283571
  `Provision your DB: ${import_picocolors20.default.bold("npx ablo migrate")} (creates your Ablo-model tables + the adapter tables; keep your own migrations for everything else), then mount ${import_picocolors20.default.bold(`${abloDir}/data-source.ts`)} at ${import_picocolors20.default.bold("/api/ablo/source")}`
@@ -283440,7 +283670,7 @@ export {};
283440
283670
  }
283441
283671
  function generateEnv(storage, opts = {}) {
283442
283672
  const { includeApiKey = true } = opts;
283443
- const databaseBlock = storage === "direct" ? "# DEPRECATED direct connector. The client registers this connection (sent once\n# over TLS, stored sealed) so Ablo dials into your Postgres. Prefer a signed Data\n# Source endpoint; to keep the transaction log in your infra too, self-host the engine.\n# Use a dedicated non-superuser role; the browser never sees this value.\nDATABASE_URL=postgres://user:password@host:5432/db\n" : "# Used by ablo/data-source.ts (your DB endpoint) + `ablo migrate` \u2014 NOT the client.\n# Ablo never sees it; the browser never sees it. Your DB stays in your app.\nDATABASE_URL=postgres://user:password@host:5432/db\n";
283673
+ const databaseBlock = storage === "direct" ? "# DEPRECATED direct connector. The client registers this connection (sent once\n# over TLS, stored sealed) so Ablo dials into your Postgres. Prefer logical\n# replication (`npx ablo connect`); to keep the transaction log in your infra too,\n# self-host the engine. Use a dedicated non-superuser role; the browser never sees this.\nDATABASE_URL=postgres://user:password@host:5432/db\n" : storage === "replication" ? "# Used by `npx ablo connect` to set up + register logical replication \u2014 the\n# DIRECT (un-pooled) endpoint. Ablo TAILS your WAL from here; it never writes.\n# The client never sees it; the browser never sees it. Your DB stays yours.\nDATABASE_URL=postgres://user:password@host:5432/db\n" : "# Used by ablo/data-source.ts (your DB endpoint) + `ablo migrate` \u2014 NOT the client.\n# Ablo never sees it; the browser never sees it. Your DB stays in your app.\nDATABASE_URL=postgres://user:password@host:5432/db\n";
283444
283674
  const webhookBlock = storage === "endpoint" ? "# Signing secret for the webhook receiver (app/api/ablo/webhooks/route.ts).\n# Ablo mints this when you register the endpoint's URL (POST /v1/webhook_endpoints\n# or the dashboard) and returns it once \u2014 paste it here.\nABLO_WEBHOOK_SECRET=whsec_your_endpoint_secret_here\n" : "";
283445
283675
  const apiKeyBlock = includeApiKey ? "# Ablo Sync Engine \u2014 use a sk_test_ key for local dev (`npx ablo push`)\nABLO_API_KEY=sk_test_your_key_here\n" : "";
283446
283676
  return `${apiKeyBlock}${webhookBlock}${databaseBlock}`;
@@ -6,7 +6,8 @@ schema, and never migrates it**. Your application keeps writing to its own
6
6
  Postgres through its own backend, exactly as it does today; Ablo only tails the
7
7
  changes and fans the confirmed rows out to every connected human and agent. This
8
8
  is the same model ElectricSQL, PowerSync, and Zero use — a publication plus a
9
- replication slot, read-only.
9
+ replication slot. Ablo consumes the logical-replication stream; your application
10
+ continues to own the write path.
10
11
 
11
12
  > **Just trying Ablo?** You don't need a database at all to start. The hosted
12
13
  > **sandbox** can host rows in Ablo's test plane — pass an `apiKey` only and omit
@@ -94,8 +95,10 @@ Re-run it until every item is green.
94
95
 
95
96
  ### 4. Point Ablo at the database with the replication role
96
97
 
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:
98
+ Give Ablo the connection string for the **replication role** you created a
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:
99
102
 
100
103
  ```bash
101
104
  # .env — server runtime only, never the browser
@@ -156,8 +159,12 @@ Operational reality you should know up front:
156
159
  WAL accumulates and consumes disk. **Ablo monitors slot lag and WAL retention**
157
160
  and surfaces it so you're never surprised by disk pressure; an abandoned slot is
158
161
  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.
162
+ - **The role's privilege footprint is narrow and precise not a "read-only"
163
+ account.** It carries the `REPLICATION` attribute, which lets it stream the WAL
164
+ and `SELECT`; it cannot `INSERT`/`UPDATE`/`DELETE`, run DDL, or own objects, and
165
+ the recipe never grants it more. (For a security review, state it that way — a
166
+ logical-replication role is a real privilege, just a tightly-scoped one — rather
167
+ than calling it "read-only", which a reviewer will correctly push back on.)
161
168
 
162
169
  What Ablo explicitly does **not** do:
163
170
 
package/llms.txt CHANGED
@@ -122,9 +122,11 @@ A schema is model fields and relations. Advanced schema helpers such as `mutable
122
122
 
123
123
  ## Storage Boundary
124
124
 
125
- `databaseUrl` is an OPTIONAL, server-only constructor option on `Ablo(...)`. It is NOT auto-read from the environment pass it EXPLICITLY to register your Postgres directly. Omit it when you expose a signed Data Source endpoint, or when trying Ablo against the hosted sandbox (apiKey only). A `DATABASE_URL` set for another tool (Prisma, Drizzle, docker-compose) is ignored unless you pass `databaseUrl` explicitly.
125
+ THE ONE PATH IS LOGICAL REPLICATION. In production every schema model is backed by YOUR OWN database, and Ablo connects to it by CONSUMING ITS WRITE-AHEAD LOG (logical replication) the Electric/PowerSync/Zero model. Ablo tails the WAL and fans changes out; YOUR APPLICATION CONTINUES TO OWN THE WRITE PATH (your own backend, your existing API). Ablo never runs DDL on, writes to, owns, or migrates your database. Connect once with `npx ablo connect` (prints the setup SQL: `wal_level=logical`, a publication, a `REPLICATION` role) then `npx ablo connect --register`. Validate with `npx ablo connect --check`. The role's privilege footprint is narrow but real a `REPLICATION`-attributed role that streams the WAL and `SELECT`s; do NOT describe it as "read-only" (a security reviewer will push back — it is a replication privilege, just tightly scoped).
126
126
 
127
- In production, every schema model is backed by YOUR OWN database. The PRIMARY path is the connection string: pass `databaseUrl` (most users already have a Postgres — often Prisma- or Drizzle-managed for auth/audit/log tables that are NOT in the Ablo schema; Ablo syncs a SUBSET of models against it). Most users do NOT run `ablo migrate` they ADOPT existing tables with `npx ablo pull` / `npx ablo check`, or keep managing tables with their own migration tool. Run `npx ablo migrate` only when Ablo should OWN the tables (it provisions the synced-model tables in your DB). The alternative to the connection string is a signed Data Source endpoint that hands Ablo an ORM `adapter` (Drizzle is the default; Prisma and Kysely are also supported) — it owns the transaction, exactly-once idempotency, and outbox in ONE pass (no hand-written `commit`/`events`); use it when database credentials must never leave your infrastructure (or for a local/private-range DB, which Ablo's cloud cannot reach over the network).
127
+ FALLBACK (only when the database can't grant a `REPLICATION` role, e.g. a locked-down managed DB): a signed Data Source endpoint that hands Ablo an ORM `adapter` (Drizzle default; Prisma and Kysely supported) — it owns the transaction, exactly-once idempotency, and outbox in ONE pass; your credentials never leave your infrastructure.
128
+
129
+ DEPRECATED — being removed, do NOT recommend or scaffold: the `databaseUrl` constructor option (the dial-in, where Ablo held a read/write connection and committed directly to your DB). The end-state constructor is `Ablo({ schema, apiKey })`; the database is connected out of band via `ablo connect`, not through `databaseUrl`. (Note: `ablo init` still defaults to `--storage direct` scaffolding `databaseUrl` — that default is itself slated to lead with `ablo connect`; prefer logical replication regardless of what `init` scaffolds.)
128
130
 
129
131
  GOTCHA the user WILL hit: `DATABASE_URL` must use a NON-superuser, NON-BYPASSRLS role (Ablo enforces row-level security; owner roles are rejected with `database_role_cannot_enforce_rls`). Neon's and Supabase's default dashboard connection strings use the database OWNER (e.g. `neondb_owner`) and are rejected. EASIEST: `npx ablo migrate` detects the unsafe role and creates the scoped one automatically from the user's machine (owner credential never reaches Ablo; new DATABASE_URL written to the env file). Manual alternative — create a scoped role first: `CREATE ROLE ablo_app LOGIN PASSWORD '...' NOSUPERUSER NOBYPASSRLS; GRANT CREATE, CONNECT ON DATABASE <db> TO ablo_app; GRANT CREATE, USAGE ON SCHEMA public TO ablo_app;` — then swap user/password into the same host/db string.
130
132
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "The Collaboration Layer For AI Agents",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",