@abloatai/ablo 0.30.2 → 0.31.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,41 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.31.0
4
+
5
+ ### Minor Changes
6
+
7
+ Ablo's relationship with your database is unusual, and this release is about making the setup match it. You write through Ablo — `ablo.<model>.create`, `.update`, `.delete` — and Ablo lands each change in your own Postgres through a scoped role, then reads it back off your write-ahead log to confirm it landed. Your rows never leave your database; Ablo keeps only the ordered log that lets everyone writing to that database take turns. So "connecting a database" really means handing Ablo two narrow keys: one role that can stream and read, and one that can write rows and nothing else.
8
+
9
+ Handing over those keys used to be a chore — paste some SQL, wire two connection strings into your environment, then run a second command to register them. This release folds it into one step. Point `ablo connect --apply` at an admin connection — pass it with `--url`, or let it use the `DATABASE_URL` you already have — and it runs the whole ceremony: creates the two scoped roles, publishes your tables, turns on logical decoding where the provider allows, and registers both roles with Ablo. The admin credential is used once and discarded. Nothing is written to your `.env`; Ablo generates and holds the scoped passwords itself, and your app keeps only its `ABLO_API_KEY`. When a password needs replacing, `ablo connect --rotate` mints a new pair and re-registers them in the same step.
10
+
11
+ The shape is deliberate, and it follows one rule: a credential can only have one owner. The admin connection is yours — it's the key to your whole database, so Ablo uses it once and never stores it. The two scoped roles are Ablo's, which is why Ablo, not you, generates their passwords: a credential Ablo owns is one it can rotate, revoke, and audit, and one you can read as plain SQL before you grant it. The same rule is why the scoped strings moved out of the generic `DATABASE_URL` into names of their own — `ABLO_REPLICATION_DATABASE_URL` for the reader, `ABLO_WRITE_DATABASE_URL` for the writer. Nearly every app already sets `DATABASE_URL` to reach its own database, so a replication-only credential left there breaks the app's writes, and a `--check` that quietly falls through to it can report success against the wrong database entirely. `--check` still reads the old name, with a warning, through 0.31.x, and stops in 0.32.0; `--register` refuses it today. The one job `DATABASE_URL` keeps is the honest one: the one-time admin input to `--apply`.
12
+
13
+ Where this is heading: connecting should eventually be a single sentence to an agent — "connect my Postgres" — with the CLI recognizing your provider, doing everything your credentials allow, and asking once for the single privileged step it can't do for you (a reboot to enable logical replication, an OAuth grant). `--rotate` is the first piece of the credential lifecycle that vision needs, and the `ABLO_`-prefixed names clear away the last fossils of the old dial-in era. The direction is steady: fewer keys, held more briefly, with the one irreversible act always surfaced for a human to approve.
14
+
3
15
  ## 0.30.2
4
16
 
5
17
  ### Patch Changes
6
18
 
7
- - **`ablo connect --apply` sets up your database for you instead of printing SQL to run.** It creates the scoped replication and writer roles, publishes your tables, enables logical replication where your provider allows it, and repoints `DATABASE_URL` and `ABLO_WRITE_DATABASE_URL` at the new least-privilege roles — then verifies by reconnecting as the replication role and running the readiness checklist. It uses the admin credential already in `DATABASE_URL`, on your machine only, after a plain-language confirmation; the generated role passwords are written to your environment file, never printed. Every step is idempotent, so it is safe to re-run, and on a managed provider that needs a restart to finish enabling logical replication (RDS, Neon) it applies everything else and leaves that one step with the exact fix. `ablo connect` with no flag still prints the recipe to run by hand; add `--show-sql` to preview the exact statements before applying.
19
+ - **`ablo connect --apply` sets up your database for you.** Connecting a database used to mean pasting a block of SQL into a psql prompt. Now one command does it. `--apply` creates the two scoped roles, publishes your tables, turns on logical replication where your provider allows, and points `DATABASE_URL` and `ABLO_WRITE_DATABASE_URL` at the new least-privilege roles. Then it checks its own work, reconnecting as the replication role to confirm the database is ready.
20
+
21
+ Nothing leaves your machine. It runs against the admin credential already in `DATABASE_URL`, asks before it changes anything, and writes the passwords it generates to your environment file rather than printing them. Every step is safe to run twice. When a managed provider needs a restart to finish — RDS, Neon — it does everything else and hands you that one step, with the exact fix. Prefer to run the SQL yourself? `ablo connect` still prints it, and `--show-sql` shows it inline first.
8
22
 
9
23
  ## 0.30.1
10
24
 
11
25
  ### Patch Changes
12
26
 
13
- - **Writes land in your own database on a connected source.** Once you register a database with `ablo connect`, the models you already read from its replication stream become writable: `ablo.<model>.create`, `.update`, and `.delete` route through Ablo's commit chokepoint and into your database, with no connection string in the client. Coordination travels with the write — a claim you hold is checked at the chokepoint before the change is applied, so agents and people serialize on a row whether they reach it by reading from Ablo or by writing back to your own database.
27
+ - **Writes land in your own database.** Connect a database with `ablo connect` and the models you already read become writable. `ablo.<model>.create`, `.update`, and `.delete` route through Ablo's commit chokepoint and into your database no connection string in the client. A claim you hold is checked at the chokepoint before the write applies, so agents and people take turns on a row whether they reach it by reading from Ablo or by writing back to your own database.
14
28
 
15
- A write resolves as soon as it is durably accepted, which the receipt reports as `queued`. Ask for the authoritative outcome with `wait: 'confirmed'` Ablo watches the change actually land in your database (the write-ahead-log echo for a replicated source, or the event feed for a signed data-source endpoint) and promotes the receipt to `confirmed` before the call returns. The client call is identical across both connection shapes; only the feed Ablo watches for the confirmation differs.
29
+ A write returns as soon as it is durably accepted; the receipt calls that `queued`. For the authoritative outcome, pass `wait: 'confirmed'`. Ablo watches the change actually land in your database the write-ahead-log echo for a replicated source, the event feed for a signed endpoint and promotes the receipt to `confirmed` before the call returns. The code is the same either way; only the feed Ablo waits on differs.
16
30
 
17
- - **A clear readiness error when a model's table isn't provisioned yet.** Registering a schema with `ablo push` records your models, but a plane's physical tables are created separately, out of band — so a model can exist in the registered schema before its table does. Reading or writing such a model now returns a typed `model_not_provisioned` error (HTTP 409) that names the gap and points you at provisioning the plane's tables, rather than surfacing as an opaque failure.
31
+ - **A clearer error when a table isn't ready yet.** `ablo push` records your models, but a plane's physical tables are created separately, out of band — so a model can exist in the schema before its table does. Read or write one in that window and you now get a typed `model_not_provisioned` error (HTTP 409) that says what's missing and how to fix it, instead of an opaque failure.
18
32
 
19
33
  ## 0.30.0
20
34
 
21
35
  ### Minor Changes
22
36
 
23
- - **Removed the deprecated `databaseUrl` client option your database connects to Ablo out of band, not through the client.** Ablo is Stripe-shaped: the client only ever talks to the engine. You construct it with `Ablo({ schema, apiKey })` (add `transport: 'http'` for a stateless server worker), and it reaches Ablo over WebSocket or HTTP it never opens a Postgres connection itself. How your data connects is a separate step, set up once, in one of two shapes:
37
+ - **The `databaseUrl` client option is gone.** Your database connects to Ablo out of band, not through the client. Ablo is Stripe-shaped: the client only ever talks to the engine. You build it with `Ablo({ schema, apiKey })` add `transport: 'http'` for a stateless worker and it reaches Ablo over WebSocket or HTTP. It never opens a Postgres connection of its own. How your data connects is a separate, one-time step, and it takes one of two shapes:
24
38
 
25
- - **Logical replication the default.** `ablo connect` registers your database and Ablo tails its write-ahead log. Your rows stay in your database; Ablo reads the change stream and keeps its own transaction log. Nothing in the client holds a connection string.
26
- - **Signed data-source endpoint the fallback.** For a database that cannot grant a `REPLICATION` role, your app exposes a signed HTTP endpoint that Ablo calls. Same out-of-band principle: the credentials live with your app, not in the Ablo client.
39
+ - **Logical replication, the default.** `ablo connect` registers your database, and Ablo tails its write-ahead log. Your rows stay where they are; Ablo reads the change stream and keeps its own log.
40
+ - **A signed endpoint, the fallback.** When a database can't grant a `REPLICATION` role, your app exposes a signed HTTP endpoint for Ablo to call. Same principle the credentials stay with your app.
27
41
 
28
- Because the client no longer carries a connection string, `ablo init` no longer scaffolds the `--storage direct` connector the last surface that emitted `databaseUrl: process.env.DATABASE_URL` into a generated client. Run `ablo init --storage replication` (the default) or `--storage endpoint` instead.
42
+ With no connection string in the client, `ablo init` no longer scaffolds the `--storage direct` connector, the last thing that wrote `databaseUrl: process.env.DATABASE_URL` into generated code. Run `ablo init --storage replication` (the default) or `--storage endpoint`.
29
43
 
30
- - **The claim work label is `description`, not `reason`.** A claim is an advisory coordination lock: you take it on a row before editing so other participants — agents or people — see the row is held and back off. The free-text label that says _what_ you are doing is now `description`, the text a peer reads when it hits a row you hold: `ablo.<model>.claim({ id, description: 'Renaming the task to match the brief' })`. The former `reason` option is removed with no alias, so a call still passing `reason` no longer type-checks — rename it to `description`. This is a different field from the `reason` on a claim _rejection_ or _lost_ event (`'conflict'`, `'expired'`, `'preempted'`), which says _why_ coordination denied or ended a claim and is unchanged.
44
+ - **A claim's work label is `description`, not `reason`.** A claim is an advisory lock: you take it on a row before editing, and other participants — agents or people — see it's held and wait. The free-text note that says what you're doing is now `description`, the line a peer reads when it lands on a row you hold: `ablo.<model>.claim({ id, description: 'Renaming the task to match the brief' })`. The old `reason` option is gone, with no alias, so a call still passing it won't type-check — rename it. (This is a different field from the `reason` on a rejected or lost claim `'conflict'`, `'expired'`, `'preempted'` which explains why coordination said no, and hasn't changed.)
31
45
 
32
- - **Claim a key Ablo hasn't synced: `ablo.<model>.claim(id)`.** Claims now come in two shapes that mirror whether Ablo holds the row. The object form `claim({ id })` loads the row from Ablo's synced pool and returns a `HeldClaim<T>` carrying `.data`. The new string overload `claim(id)` locks a row that lives _only_ in your own database — Ablo has never synced it so it skips the pool load and the `entity_not_found` throw and returns a `HeldLease`: a `HeldClaim` without `.data`. This is Rung 0 of the connect ladder — advisory coordination with no replication grant, no schema change, and no `organization_id` column, so you can coordinate agents against your database before wiring any sync. `await using` auto-releases either shape at end of scope, and the object form is unchanged, so no existing caller shifts.
46
+ - **Claim a key Ablo hasn't synced: `ablo.<model>.claim(id)`.** Claims now come in two shapes, matched to whether Ablo holds the row. The object form, `claim({ id })`, loads the row from Ablo's synced pool and returns a `HeldClaim<T>` with its `.data`. The new string form, `claim(id)`, locks a row that lives only in your database — one Ablo has never synced — skipping the pool load and the `entity_not_found` throw, and returns a `HeldLease`: a `HeldClaim` without the data. This is Rung 0 of the connect ladder: coordination with no replication grant, no schema change, no `organization_id` column so you can coordinate agents against your database before wiring any sync at all. `await using` releases either shape at end of scope, and the object form is untouched, so nothing you've already written changes.
33
47
 
34
48
  ### Patch Changes
35
49
 
36
- - **`ablo push` defers provisioning instead of failing when the plane's role cannot run DDL.** When the engine role bound to a plane lacks the privilege to create or alter tables, `push` no longer aborts the whole command. It records the schema and defers the provisioning step, so your models still register and the table changes are applied later by a role that can run them — rather than blocking the push on a privilege the plane may never grant the engine directly.
50
+ - **`ablo push` defers provisioning instead of failing when a role can't run DDL.** When the engine role bound to a plane can't create or alter tables, `push` no longer aborts. It records the schema and defers the table changes for a role that can run them — so your models still register, rather than the whole command stalling on a privilege the plane may never grant the engine.
37
51
 
38
- - **Schema-drift no longer false-alarms for `selectModels` / `omitModels` projection clients.** A projected client deliberately carries a subset of the full schema, so its schema hash legitimately differs from the unprojected one. The drift check now compares against the projection's own source hash, so a correctly-projected app no longer sees a spurious drift warning at bootstrap.
52
+ - **Schema-drift stops false-alarming on `selectModels` / `omitModels` clients.** A projected client carries a deliberate subset of the schema, so its hash differs from the full one by design. The check now compares against the projection's own source hash so a correctly projected app no longer sees a phantom drift warning at startup.
39
53
 
40
54
  ## 0.29.3
41
55
 
package/README.md CHANGED
@@ -430,12 +430,11 @@ each other's changes in real time — that's the default, not a feature you turn
430
430
  - `useAblo(...)` gives React clients the live row, kept current automatically.
431
431
  - `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.
432
432
 
433
- Your application owns the write path: writes that land in your database — through
434
- your own backend, your existing API, any path are tailed off the WAL and fanned
435
- out to connected clients automatically (the Electric model). The SDK model
436
- methods (`ablo.<model>.create/update/delete`) and the HTTP write endpoint are the
437
- *JavaScript/agent* ergonomic surface they participate in claims and confirmation
438
- — but they are not a requirement for your normal writes to show up.
433
+ Writes go through Ablo. `ablo.<model>.create/update/delete` and the HTTP write
434
+ endpoint enter Ablo's commit chokepointwhere claims, ordering, and idempotency
435
+ are enforced and Ablo lands the change in your database. It then tails the WAL to
436
+ confirm the row landed and fans the confirmed change out to every connected client.
437
+ One surface for humans, servers, and agents; one place coordination happens.
439
438
 
440
439
  ## HTTP Writes
441
440
 
@@ -458,19 +457,18 @@ curl https://api.abloatai.com/api/v1/commits \
458
457
 
459
458
  ## Your Database
460
459
 
461
- In production, every schema model is backed by **your own database** Ablo
462
- **consumes** it, never owns it. The model is Electric's (and PowerSync's, and
463
- Zero's): Ablo consumes your Postgres' logical-replication stream and fans the
464
- changes out; **your application continues to own the write path.** Two ways Ablo
460
+ In production, every schema model is backed by **your own database**, and that's
461
+ where your rows live. You write through Ablo; it lands each change in your Postgres
462
+ through a scoped role, then tails the WAL to confirm it and fan it out. Ablo holds
463
+ the ordered transaction log and coordination never your rows. Two ways it
465
464
  connects:
466
465
 
467
466
  | | How Ablo connects to your Postgres | Use when |
468
467
  | --- | --- | --- |
469
- | **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). |
470
- | **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). |
468
+ | **`ablo connect`** (primary) | Sets up logical replication and a scoped writer role (`npx ablo connect --apply` does it end to end). Ablo writes your rows through the writer role and reads them back over the WAL to confirm — it writes rows but runs no DDL and owns no schema. | Your database can grant a `REPLICATION` role (most can). |
469
+ | **Signed endpoint** (fallback) | Your app exposes one route built from an ORM adapter (`prismaDataSource` / `drizzleDataSource`); Ablo writes and confirms through it. Needs no replication setup. | Your database **can't** grant a replication role (a locked-down managed DB). |
471
470
 
472
- Your database is the system of record — Ablo holds the transaction log and
473
- coordination, never your rows. See
471
+ Your database is the system of record. See
474
472
  [Connect Your Database](./docs/data-sources.md).
475
473
 
476
474
  ## Configuration