history and undo
briven installs a change log into your own database at provisioning time: a row for every insert, update and delete, recording who did it. you read it with ordinary sql. you undo with the dashboard.
what it is
It is not a service sitting beside your database watching it. It is a set of triggers and tables installed inside the database itself, in a schema called _briven, put there when the database is provisioned. Schema version 2. Every insert, update and delete on a tracked table writes a second row recording what changed, when, and who was connected at the time.
Because it lives in your database, it is yours: you can query it, join against it, and take it with you in a dump. Nothing has to be switched on in an external console for the log to exist.
The objects it installs are _briven.changes, _briven.tracked, _briven.restore_points and _briven.meta, plus a row trigger named _briven_log and a notify channel named briven_changes.
the change log, column by column
_briven.changes
id -- monotonic; order by this, not by time txid -- the transaction the change belonged to changed_at -- clock_timestamp(), so it differs per row statement_at -- statement_timestamp(), the same for a whole statement schema_name table_name op -- 'INSERT' | 'UPDATE' | 'DELETE' | 'TRUNCATE' pk -- the primary key of the row that changed old_row -- the row before, as json new_row -- the row after, as json actor -- a claim, set by whoever ran the sql actor_source -- 'setting' | 'role' actor_role -- the session user; not forgeable undone_at undone_by_change_id undo_of_change_id
The two timestamps are not redundant. changed_at is clock_timestamp() and therefore differs between rows written by the same statement; statement_at is statement_timestamp() and is the same for all of them. That is how you tell “one update that touched nine hundred rows” apart from “nine hundred updates”.
reading it, from sql
You can read your own change log with an ordinary SELECT, from psql, from your ORM, or over sql over http — it starts with SELECT, so it passes the read-only allowlist.
sql
SELECT id, changed_at, schema_name, table_name, op, pk, old_row, new_row, actor FROM _briven.changes ORDER BY id DESC LIMIT 50;
Your role holds SELECT and only SELECT on _briven.changes, _briven.tracked and _briven.restore_points. Reading is free and unrestricted; writing to the log is not something you or anything you run can do.
what sql cannot do
You cannot create a restore point, undo a change, or run a restore over SQL. Those functions are invoker-rights and your role has no INSERT or UPDATE on the _briven schema, so they will refuse. They are dashboard only, at /history. There is no /api/v1 route for history, undo or restore points — not an undocumented one, none.
The reason to say that flatly rather than leave it implied: a reader who assumes an endpoint exists will write a script around it, and discover during a real incident that the script was never going to work. Reading is programmable. Acting is a person, in a browser, looking at what they are about to do.
who changed the row
The log records who, not merely that. This is the thing it does that an ordinary audit trigger does not, and it is the reason the feature was worth building rather than borrowing.
Be clear-eyed about the two halves. actor is a claim — anyone who can run SQL against the database can set it to anything, so treat it as a label your own application attaches, not as proof. actor_role is the session user PostgreSQL saw, and that half cannot be forged. When the two disagree, believe actor_role.
what undo cannot do
Undo replays rows backwards. Everything below follows from that one sentence, and every one of these is a real refusal you can hit.
rows, never ddl. A DROP COLUMN, a CREATE TABLE or a type change leaves no change row at all, so there is nothing to put back. Undo is for data you deleted, not for schema you changed.
a table with no primary key cannot be undone. Its changes are still logged, and you can still read them — but with no key there is no way to say which row to put back, so the undo refuses rather than picking one.
a changed schema refuses a restore. A restore point stores a fingerprint of the schema — the columns, their types, their nullability, whether they are generated. If that fingerprint no longer matches, the restore is refused. That refusal is the feature: replaying old rows into a table whose shape has moved is how you turn one bad afternoon into two.
caps. A single undo call replays at most 10,000 changes. The history and diff pages show 500 rows.
retention. Changes are kept for 30 days by default. Pruning marks any restore point unusable once the changes it depends on have aged out, so a restore point never silently becomes a half-restore.
Two refusals surface only once a replay is already running: state_mismatch, meaning the row has been changed again since, and superseded. Undo stops rather than guessing. That is deliberate — an undo that quietly overwrites somebody else’s work is worse than no undo at all.
One more small one: restore point names are unique per database, and a clash is refused as name_taken.
restore is a branch plus a rewind
Restoring a named restore point does not rewind your database. briven takes a branch of the parent and replays the changes backwards on the copy. The original is never written to. If the rewind fails partway, the branch is destroyed and you still have exactly what you had before you asked.
The cost is the branch’s cost: on the shared tier the parent’s sessions are briefly disconnected to take the copy, for the same PostgreSQL reason described on the branching page.
This is restore to a named restore point, not point-in-time restore. There is no “take me back to 14:07:33” here. Continuous recovery to an arbitrary timestamp is a separate, operator-run disaster-recovery process, not a self-service feature — do not plan around it as if it were one.
which plans have it
History is not available on the free plan at all. On starter, pro and enterprise it is unlimited.
The reason is write amplification, not a sales lever: every insert, update and delete on a tracked table writes a second row, so a database with history on does roughly twice the write work. On the free tier, where the hardware is shared, that cost lands on everybody else. That is the real reason, and it is the one we will give you if you ask.
Related reading: branching for the copy mechanism a restore uses, sql over http for querying the log without a driver, connecting for psql and your ORM, and the management api for the things that do have endpoints.