Limitations
What dbtrail does not capture, cannot undo, or refuses to rebuild, and why. Read this before you rely on it.
dbtrail is a safety net for data changes, and a safety net is only useful if you know where its edges are. This page lists every limit worth knowing, grouped by the question you would actually ask. Some are deliberate design boundaries, some are gaps we have not closed yet. Each entry says which.
How to read this page
By design means the behaviour is intended and will not change. Not yet means it is a known gap; the linked issue tracks it. When dbtrail hits one of these limits it refuses out loud (an error, a warning, a status word) rather than returning something that looks complete but is not. If you ever see a silent partial result, that is a bug: report it.
What gets captured
Supported databases
| Source | Status | Notes |
|---|---|---|
| MySQL 8.0 and 8.4, Percona Server | Supported | Includes RDS, Aurora, Cloud SQL |
| PostgreSQL 14+ | Beta | Separate capture binary, nothing installed in your server. Beta limitations |
| MariaDB 10.6+ | Alpha | Alpha limitations |
The index database is always MySQL 8.0 or later (by design). Every source, PostgreSQL and MariaDB included, writes into the same MySQL index schema. Pointing the index at a PostgreSQL or MariaDB server is not supported. The bundled Compose stack ships a MySQL 8.4 index container so you do not have to run one yourself; a MySQL you already operate works too.
Source settings that must be right
- Row-format binlog with full row images.
binlog_format=ROWandbinlog_row_image=FULLare required. WithMINIMALorNOBLOBthere is no before-image, so nothing can be undone. Add server checks this and the stream refuses to start otherwise. (Managed MySQL defaults are fine.) - Partial JSON logging is skipped, not captured (not yet). If the source
sets
binlog_row_value_options=PARTIAL_JSON, MySQL logsJSON_SET,JSON_REPLACEandJSON_REMOVEupdates as diffs dbtrail cannot apply. Those updates are skipped with a warning in the daemon log, and the Status view's doctor check warns about the setting. Clear it on the source (SET PERSIST binlog_row_value_options = '';) to capture JSON updates in full. - A crash on the source can lose the binlog tail (by design, a MySQL
property). With
sync_binlogother than1, an OS crash can drop committed transactions before they were ever written to the binlog. dbtrail cannot capture what MySQL never logged. The doctor check warns; verification is the only way to notice it later.
Tables that are left out
- Tables without a primary key, and tables not on InnoDB, are excluded from capture (by design). Without a primary key dbtrail cannot tell two identical rows apart, and non-InnoDB engines are not transactional, so interrupted statements leave half-logged changes. The schema snapshot skips these tables and records the reason; they never show up in Events. The quickstart has the queries that find them.
- Schema changes need a fresh snapshot. After an
ALTER TABLE, events for that table are skipped with a warning until the schema snapshot catches up. The daemon re-snapshots automatically when it sees DDL, so the window is normally seconds. A rename or a drop-and-add of a column at the same position, seen after the snapshot time, stops capture for that table with an error until you re-snapshot.
Changes that leave no row events
TRUNCATE TABLE, DROP TABLE and RENAME TABLE write no row-level events to
the binlog (by design, again a MySQL property). dbtrail records that they
happened, with a timestamp, under schema changes, but there are no rows to
undo. The only way back is a rebuild from a backup taken before the statement:
Backups → Build a .sql backup for any moment. Any rebuild that crosses one
of these statements refuses instead of resurrecting rows that no longer exist.
On MySQL before 9.6, ON DELETE CASCADE and ON UPDATE CASCADE child rows
are also invisible: InnoDB applies them inside the storage engine and logs
nothing for them. Restore detects this and repairs the children
automatically (the CASCADE banner), with two limits: a multi-column foreign
key is skipped and flagged rather than guessed, and a parent key that moved
twice inside the window is reported as INCOMPLETE RECOVERY. See the
recovery guide.
Statement text
The originating SQL statement (shown by the EE forensics views) is captured
only when the source enables it (binlog_rows_query_log_events=ON), and it is
stored up to 16 KiB per statement. Longer statements are cut and marked as
truncated.
Undoing changes (Restore)
- dbtrail never runs the undo for you (by design). Restore produces a SQL script; you review it and apply it with your own client. The console has no write path to your database.
- Triggers fire again when you apply the script (by design, a MySQL
property). Re-inserting a deleted row runs the table's
AFTER INSERTtriggers like any insert would. MySQL has no session switch to suppress triggers; PostgreSQL does, and the PostgreSQL undo can use it. AUTO_INCREMENTis not put back. The rows come back with their original ids, but the counter for the next id is not rewound. The script appends a commented-outALTER TABLE ... AUTO_INCREMENTyou can fill in if it matters.- Result caps. The Events view shows 100 changes by default and up to
1,000; an undo script covers up to 1,000 changes by default and 10,000 at
most. Narrow by table, row or time to stay under the cap, or use the CLI
(
recover --limit) for larger scripts.
Rebuilding a table or a row at a moment (time-travel)
A rebuild merges a saved backup with the changes recorded after it. These limits follow from that.
- You need a backup taken at or before the moment you want. Without one, only rows that changed inside the retained history can be shown; rows that never changed are unknown. The console says which backup it used, and refuses a full-table rebuild rather than returning a partial table that looks complete.
- Some primary key types cannot be matched (by design, refused). The join
between a backup row and its recorded changes is done on the primary key, so
the key must be written the same way on both sides. Supported: integers,
strings,
ENUM/SET,DATETIME/TIMESTAMP/DATE,YEAR,DECIMAL, and theBINARY/VARBINARY/BLOBfamily. Refused:FLOAT,DOUBLE,TIME,BIT,JSON, spatial types, and a generated column inside the key. - A primary key that changed value inside the window stops the rebuild. An
UPDATEthat rewrites the key makes the row untraceable; the rebuild refuses and names the row. - A column added or dropped after the backup stops the rebuild. Emitting the table would silently mix two schemas. Take a new backup after the change.
- The cut point for a single row is transaction-safe; for a whole table it is
per row (not yet). Single-row time-travel includes or excludes each
transaction as a whole. The full-table rebuild (the
.sqlexport and the time-travel SQL port's table queries) still cuts per row, so a transaction that straddles the requested second can be half-applied. Recorded timestamps have one-second resolution. ENUMandSETvalues are decoded with the schema in effect at each change. A change made between anALTERand the next snapshot decodes with the old definition; a value the definition does not know is shown as the raw number.- Time-travel SQL through ProxySQL is in beta: single-column primary keys, one equality predicate, no joins or aggregates inside the query. Beta limitations.
Verification
The Verification page proves the rebuild chain works, within bounds. In
short: a MATCH is a strong signal, not a certificate. The fingerprint it
compares is not cryptographic, it checks row content only (not indexes,
triggers or AUTO_INCREMENT), the default mode does not exercise the undo
inputs (that is a separate check on the same page), and comparing against the
live database needs a table nobody is writing to. Full detail in the
verification guide.
Retention and storage
- History is only as long as you keep it. Rotation drops old hourly partitions from the index. Archiving them to Parquet (local or S3) keeps them queryable; without archiving, rotated changes are gone. Set the destination under Manage servers → Edit → Advanced and the policy under Rotation.
- The oldest moment you can rebuild to is the later of two floors: the oldest backup you still have, and the oldest change still kept. Overview shows restore coverage per table; there is no single number for the combined floor yet.
- You operate the index database (by design, and the whole point of the paid plans). Disk, backups of the index itself, upgrades and corruption of the index MySQL are yours in the free core. Support scope.
- Several servers sharing one index share the live partitions. When archives cannot be attributed to a single server, backup staleness is reported as not evaluable rather than guessed either way.
Who changed it (forensics, EE)
- Attribution is MySQL-only (by design, settled). PostgreSQL's replication stream carries no session id, and dbtrail installs nothing in a source to get one. Undo, time-travel and verification work the same for PostgreSQL; only "which session did this" is missing.
- Exact attribution needs an audit plugin on the source. Without one, the
performance_schemafallback is best-effort: short history, no time-of-day filtering, cannot be enabled after the fact. How the tiers differ.
Security boundaries
These are design decisions, listed so you can plan around them.
- dbtrail connects to your source as a read-only replication client and never writes to it, never installs anything in it, and never runs a generated script.
- The console binds to localhost in the bundled stack. To expose it, set a
password first and serve it over HTTPS, either with its own certificate
(CLI:
--console-tls-cert/--console-tls-key) or behind a reverse proxy. - The time-travel SQL port speaks plain MySQL protocol with no TLS. Keep it on localhost or terminate TLS at ProxySQL.
- The Claude connection (MCP) is read-only and cannot point the console at a different database.
Something missing?
If you hit a limit that is not on this page, or one listed as "not yet" that blocks you, open an issue. Limits that block real workloads move up the list.