PostgreSQL 18 with surgical, dependency-aware undo of live transactions.
Reverse one bad transaction, or a whole incident's worth, down to the exact rows it touched,
on a live database, with a clear "is this safe?" answer every time. Even when an agent did it.
Quickstart · How it works · CLI · For AI agents · Deployment · Architecture
A migration zeroes 1,200 invoice amounts at 14:00. You catch it at 14:40. A PITR restore to 13:59 brings the amounts back and discards forty minutes of orders, payments and signups that had nothing to do with the migration. EterDB reverses the 1,200 rows and keeps the forty minutes.
| Backup or PITR restore | EterDB undo | |
|---|---|---|
| Recovers | the whole database as of a past moment | the rows one transaction touched |
| Discards | every write after that moment | nothing else |
| The database | comes back from a copy | stays live throughout |
| Writes that read the bad value | indistinguishable from the rest | listed before you decide |
It records the before and after image of every write to a tracked table, computes the compensating transaction for one transaction id, and applies it atomically against the live database. A human or the agent that made the change can run it, which matters now that agents write most new production database code and a committed transaction is treated as a permanent one.
Point-in-time recovery is inside EterDB as the storage tier: pg_basebackup base backups plus
archived-WAL replay, in unprivileged containers. It brings a dropped table back, since a
DROP leaves no row to reverse. Branches and forks are the other half of the toolkit and protect
a change before it ships; this is for after.
This repository is the open-source engine, CLI, and sidecars. The hosted multi-tenant control plane is not part of it.
brew install eterdb/tap/eter # or: curl -fsSL https://eterdb.com/install.sh | sh
eter demoeter demo starts the container stack (Docker required) and walks the seeded 2:00 PM
incident: a migration forgets its WHERE clause and zeroes every invoice. You investigate with
eter log, preview the reversal, and undo it while unrelated writes survive. Act two drops the
orders table and recovers it from a base backup.
To point EterDB at your own data instead, see Deployment.
A sidecar reads the engine's WAL through logical decoding and records the row before and the row
after, for every write to a tracked table, into a metadata store outside your database. Reads are
captured separately, inside the engine, as (table, primary key): a read leaves no row behind, so
nothing outside the process can see one.
eter preview <txid> builds the dependency graph for one transaction out of that store. If
nothing has overwritten or read the rows it wrote, the transaction is clean, and
eter undo <txid> --apply inverts the recorded before-images into compensating DML and runs all
of it against the live database in a single transaction, restoring exactly those rows and leaving
concurrent writes alone (end-to-end in test/e2e.sh). If something has, the
transaction is dependent: the preview names every dependent transaction and marks each as
provably dependent or only coarsely so, and you choose to refuse, cascade, or reverse the target
alone.
A DROP has no before-image to invert, so dropped tables and columns take the other path: a
pg_basebackup base backup plus archived-WAL replay into a throwaway Postgres, with the object
extracted from the copy. The live database is never restored over.
Undo reverses database state. It cannot unsend an email or reverse a charge, so the preview
surfaces the external references (Stripe ch_…, message ids) in the rows it would touch before
you decide.
It is PostgreSQL 18. Read capture is the one piece that has to be in the engine, and it ships as
a patch of nine files and about 390 lines tracked against upstream, targeting one major version at
a time. The patched binary passes the PostgreSQL isolation (119/119) and regression (231/231)
suites with observe off, and thirteen extensions including pgvector build against it and pass
their own installcheck. Your driver, ORM and SQL dialect are unchanged.
A single self-contained Go binary (the engine SQL and the compose stack are embedded), built to
be agent-usable: every command takes --json and returns a stable exit code.
brew install eterdb/tap/eter # macOS / Linux
curl -fsSL https://eterdb.com/install.sh | sh # any Unix, prebuilt binary
make build # from source (needs the Go toolchain)init · track [table|--all] · log · show · preview · undo [--apply --cascade|--targeted]
cohort · undo-cohort · mark · status · doctor · guide · version · connect · disconnect
demo [up|down] · jobs · snapshot · recover-table · recover-rows · recover-column · as-of
eter connect postgres://eter:eter@localhost:5433/eter # or --url <orchestrator>
eter doctor # connection, engine, versions, recovery
eter guide # the agent playbookExit codes: 0 ok · 2 usage · 3 not found · 4 dependent (undo refused) · 5 db ·
6 config · 7 prerequisite missing · 130 interrupted.
The agent that made the change can run the reversal itself.
- The CLI is a stable machine interface.
--jsonon every command, and the exit code carries the outcome, so an agent branches on it without parsing prose. Exit4is the "this undo is not clean, a later transaction depends on it, stop and review" signal. eter guideprints the playbook: investigate witheter log, preview witheter preview <txid>, apply witheter undo <txid> --apply.- The website is agent-legible. eterdb.com/llms.txt is a curated Markdown map (llms.txt convention) of the product and architecture, for ingesting without scraping HTML.
EterDB runs as the two-container stack in docker-compose.yml:
engine: patched PostgreSQL 18 +eter_ssi, change capture via logical decoding, WAL archiving on. Your application connects here, onlocalhost:5433.control-plane: a separate metadata store (so recovery data survives the tenant having a bad day) plus the capture sidecar and the orchestrator, the one HTTP entry point (/v1,localhost:4400).
docker compose up -d # from a clone; pulls the images, --build compiles the engine
eter connect --url http://localhost:4400
eter init && eter track --all # install the engine, capture every table with a primary key
# ... your app (or an agent) runs, and something goes wrong ...
eter log # find the transaction
eter preview <txid> # clean vs. dependent, with read dependencies and external refs
eter undo <txid> --applyRead-dependency capture, the part a backup cannot see because a read leaves no row behind, needs
the patched engine, which is why engine is an image rather than an extension. docker/README.md
has the full container reference; ext/eter_ssi/README.md and pg/README.md cover building the
patch and the extension yourself.
The control-plane container also runs whole-database time-travel and recovery of dropped
objects, standard PITR (pg_basebackup base backups plus archived-WAL replay), no kernel module
and no privileged mode:
eter recover-table public.widgets # submits a managed restore job and waitsRecovery restores the pre-incident base backup into a throwaway local Postgres, replays WAL to
just before the destructive change, extracts the lost object, and merges it back into the live
database. Exposed as snapshot, recover-table, recover-rows, recover-column, and as-of.
See sidecars/storage/README.md.
ext/eter/ the engine: track / preview_undo / undo / cohort / external-refs (the frozen SQL surface)
ext/eter_ssi/ C extension: native SSI read-dependency capture + drain background worker
pg/ the observe-mode core patch (PG18) + build / validation notes
cli/ the agent-usable `eter` CLI (Go, cobra + pgx)
sidecars/capture/ logical-decoding change capture into eter.history (Go)
sidecars/storage/ base backups + WAL-replay PITR: object recovery, as-of-T reads (Go)
sidecars/orchestrator/ the single HTTP entry point: /v1 API + async restore jobs (Go)
sidecars/eterclient/ the shared Go engine client (tenant + meta-store pools)
demo/ the seeded e-commerce "2:00 PM incident"
test/ end-to-end + correctness suites (e2e, ssi, observe, false-clean, …)
docker/ container images + the container bring-up guide
site/ the public marketing website (static HTML/CSS/JS)
EterDB was built with heavy AI assistance (Claude) under human direction and review. The
correctness suites in test/ run from this repository, so every claim above is
reproducible. If one does not hold up, open an issue.
Contributions and issues are welcome. See CONTRIBUTING.md to get started and CODE_OF_CONDUCT.md for community expectations. For anything security-sensitive, follow SECURITY.md rather than opening a public issue.
Apache License 2.0: see LICENSE and NOTICE. The observe-mode patch under
pg/patches/ modifies PostgreSQL source and is governed by the PostgreSQL License where it does
so; all EterDB-original code is Apache-2.0.