v0.32.0
Minor Changes
The last release folded connecting a database into a single command. This one finishes the thought: once a database is connected, your application holds exactly one Ablo secret — ABLO_API_KEY — and nothing else.
The scoped roles Ablo needs to read and write your database are real Postgres credentials, but they were never meant to live in your environment. ablo connect --apply creates them from a one-time admin connection, hands them to Ablo, and discards the admin credential; from then on Ablo holds and rotates them. What changed in this release is the last place that still asked you to keep a database credential: the health check. ablo connect --check used to require the two scoped connection strings in your environment so it could dial them from your laptop. Now it asks Ablo to check the database it already holds, from the same infrastructure replication actually runs on, and needs nothing but your API key. If your own machine can’t reach your database — an IPv6-only host, an IP allowlist, a VPN — that no longer matters, because your machine was never the right place to check from. And when nothing is connected yet, the check says so plainly and points you at ablo connect --apply rather than failing for a missing environment variable.
The DATABASE_URL fallback for the replication credential, deprecated in 0.31.0 with a warning, is removed as promised. Reading a scoped connection string from the generic DATABASE_URL risked quietly validating against your application’s own database, so the replication role is read only from ABLO_REPLICATION_DATABASE_URL now. If you register a database by hand instead of using --apply, you set that and ABLO_WRITE_DATABASE_URL just long enough to register — Ablo takes them over, and your app is back to holding only its API key. DATABASE_URL keeps its one honest job: the transient admin credential for --apply.
The other half of holding one key is trusting that Ablo’s writer stays inside the rules your database already enforces. Ablo writes through a scoped role that is subject to your row-level security — but many applications don’t carry a tenant in a column; they set it once per request as a session variable, SET app.current_org = '…', and every policy reads that. That was invisible to Ablo: its writer opened its own transaction and never set the variable, so on the first insert every policy evaluated against an empty tenant and the write failed closed — silent, total, and correct, but baffling from the outside. You can now declare the mapping in your schema, and Ablo’s writer sets the variable per transaction before it touches a row:
defineSchema({
models: { document: { /* … */ } },
tenantContext: [{ guc: 'app.current_org', from: 'orgId' }],
})
The from side is a closed set — orgId, projectId, environment, and the other identifiers Ablo establishes for the write from your credential. It is deliberately not free-form: a mapping can only pass through a value Ablo has already authenticated, never let a caller name its own tenant, so it can narrow what the writer sees but never widen it. Settings Ablo reserves for itself are refused at definition time. Your policies stay the sole authority on what the writer may touch; this only gives them the context they were written to read.
Reading the setup SQL before you grant it is the whole compliance story of ablo connect, so the SQL has to match its narration — and in a few places it didn’t. --tables documents promised least privilege but granted the reader SELECT on every table in the schema, present and future, and the writer every sequence; both are now scoped to exactly the tables you name, with schema-wide access surviving only in whole-database mode, where it’s honest. A REVOKE CREATE ON SCHEMA public FROM PUBLIC that had crept into the script is gone — a vendor recipe must not alter the permissions of roles it doesn’t own as a side effect, and modern Postgres does it by default anyway. The row-level-security narration no longer implies protection that isn’t there: it distinguishes tables that have policies from those that don’t, and reports honest per-table coverage rather than a blanket claim. The bookkeeping table Ablo keeps in your database, ablo_idempotency, now ages its rows out on a bounded window with one documented statement to prune it, instead of growing without end. And a ledger left from an earlier integration, owned by another role, no longer makes a harmless re-run fail with must be owner of table ablo_idempotency — Postgres checks ownership before it checks whether the column being added already exists, so re-runs now touch the table only when there is genuinely something to change, and a foreign-owned ledger stops the run early with the fix spelled out.
Two smaller things. ablo connect --apply no longer reports success against a database where replication never turned on: it reads wal_level, recognizes your provider from the host, and on a managed platform that won’t accept ALTER SYSTEM shows the real path — the console toggle, the parameter group — and exits non-zero instead of printing an un-runnable statement and a green checkmark. And ablo disconnect is the counterpart to connect: it removes a database, scoped to exactly the project and environment it shows you, so tearing down a sandbox connection never reaches a sibling’s.
Where this is heading: fewer keys, held more briefly, with your database’s own rules the final word on what Ablo’s writer can do. Tenant-context mapping is the first declarative seam where your schema tells Ablo how your database governs itself; more of that — which columns a writer may set, which policies it must satisfy — will move into the schema over time, so that granting Ablo a role stays something you can read, reason about, and revoke.