Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Starf An activity monitor for Apple silicon that never invents a number e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Start here
Concepts
Release notes
What's new
Elyra
Backup and restore

Backup and restore

Félagi's state is in three places, and a backup that takes one or two of them is the problem this page exists to prevent.

Where What Lost if you skip it
MySQL Everything relational — issues, meetings and their minutes, time, whiteboards, skills Everything
storage/knowledgebase Every knowledge base article and every revision, one HTML file each Every document's prose, and all of its history
storage/app/private Attachments Every uploaded file

Skill files are columns in the database, not files, so they travel with the dump.

Why this page exists

A restore that copies the database and forgets storage/knowledgebase looks completely successful. Every row is present, the article tree draws, the titles are right, search still finds things — and every document is blank.

That is a consequence of a deliberate decision: a missing article file reads as an empty document rather than throwing, because an article has a row before it has a file. The decision is still right. It needed a counterweight, and this is it.

Taking one

php artisan felagi:backup                 # manifest + database + both file stores
php artisan felagi:backup --manifest      # tell me what to copy, copy nothing
php artisan felagi:backup --to=/mnt/backups
php artisan felagi:backup --keep=7        # delete all but the newest seven

A run writes four files, stamped with the same timestamp: a manifest, a database dump, and one archive per file store.

This is not a backup product, on purpose. A tool that appears to take a complete backup and quietly takes most of one is worse than no tool, because the operator stops thinking about it and finds out during the restore.

So it does three things it can do honestly:

  • A manifest — row counts, file counts and byte totals, written as JSON, plus the dump's SHA-256 and its per-table counts. That is the useful half: "the restore finished" and "the restore is complete" are different sentences, and a number is what separates them. The manifest is also what travels with a backup to another machine: a dump whose integrity can only be checked against itself cannot be checked at all.
  • The database, dumped as SQL and gzipped. Written by the application rather than by mysqldump — see below.
  • The file stores, archived. They are safe to copy while the application runs. Archiving is relative (tar -C), because an archive of absolute paths restores onto the machine that took it and nowhere else. A failed archive is deleted rather than left — half an archive is worse than none, because it looks like one.

If anything referenced by the database is already missing, the command warns before it writes anything. Backing up a broken state without saying so preserves the breakage and hides it in the same move.

Why the application dumps its own database

felagi:backup used to shell out to mysqldump, and dump the database only if it was on the path — saying so plainly when it was not, rather than producing an archive with a hole in it.

On the machine whose data matters, it was never on the path. Production runs PHP compiled into the application server, and that image carries neither mysqldump nor a mysql client. So every night the command said, correctly and clearly, that the database was not in the backup. The warning worked exactly as designed and nobody had a copy of the database.

The dump is therefore written in PHP, by App\Support\Backup\SqlDump, and what it writes is an ordinary SQL file: readable, greppable, and loadable by a real client wherever one exists. Nothing about it is a private format.

Two properties are worth knowing:

  • It takes no exclusions. The cache, session and queue tables go in with everything else. Every rule for deciding which tables to leave out is a rule that will one day leave out the wrong one, and a restored queue table re-running a job is a smaller problem than a restored database missing a table nobody listed.
  • It reads the file back before the run is called a success. The failure a backup actually has is a file that decompresses to nearly all of itself, and it is invisible at the moment it is written unless something reads it. That costs one gunzip a night.

The nightly backup

felagi:backup --keep=7 runs at 02:00, after the integrity check at 01:00. It is on the schedule because the previous arrangement — a command and no policy — is exactly how the machine holding the data ended up with no copy of it. Nobody runs a backup they have to remember.

--keep is passed by the scheduler and not defaulted in the command. A backup tool that deletes backups by default deletes them on a machine where somebody was keeping them by hand. Retention deletes whole sets, matched on the timestamp: removing three files of four leaves a listing that looks like a backup and restores nothing.

Seven nights on local disk is a floor, not a strategy. It survives a bad migration and a dropped table. It does not survive the disk, the machine or the building. Copying these somewhere else is still yours to arrange, and nothing here pretends otherwise.

Restoring

php artisan felagi:restore felagi-2026-09-04-020000-database.sql.gz --check
php artisan felagi:restore felagi-2026-09-04-020000-database.sql.gz
  1. Check the file with --check. It reads the whole thing, reports what the file says about itself, and touches no database.
  2. Restore the database, which drops and rewrites every table the dump holds. It asks first, and it asks even with --force.
  3. Unpack both archives to the roots the manifest records.
  4. Run the check:
php artisan felagi:check

It exits non-zero when anything is missing, so a restore script can stop instead of reporting success. Compare its output with the manifest's counts.

Why there is a restore command

An earlier version of this page argued there should not be one: loading a dump is three lines an operator can read, and the one operation where trust is least affordable should not be hidden inside a tool.

That was right, and it assumed a client. There is no mysql on the production image, so on the machine holding the data those three lines cannot be typed. The choice was never between a tool and an operator; it was between a tool and no way back.

So the command is built to be checked rather than trusted. It refuses:

It refuses Because
A file not ending in the marker a finished dump writes That is how a truncated backup announces itself instead of restoring most of the rows
A file written by another engine The schema statements are engine-specific, and half-applying them leaves a database that looks restored
A database that already has tables, without --force Restoring over a live database is not something to do by typo
Any statement that is not SET, PRAGMA, DROP, CREATE or INSERT A dump is a file, and a file can be edited. A restore loads schema and rows; it does not execute whatever arrived in something presented as a backup

And when it finishes it counts every table against what the dump says it holds, and reports the restore as failed on any disagreement. A restore that ran without throwing has proved the file was acceptable SQL, which is not the same sentence as the data is there.

--check is the mode meant to be used far more often than the other one. It answers "would this restore" without restoring, and it is what the backup command runs against every dump it writes.

When something is missing

felagi:check separates the two cases, because they are not the same:

Articles The words survive. search_text is a plain-text copy written at the same moment as the file, so the text can be recovered — see below
Revisions and attachments No second copy exists anywhere. If they are not in a backup, they are gone

An article whose file is missing says so on its own page, with what the search index still holds, and offers Recover the text.

That recovery is lossy and the confirmation says so: headings, lists, links and tables are gone, because the search index never held them. It is the difference between an empty page and a readable one, and not a substitute for restoring the file. It is behind a confirmation for that reason — somebody who does it without reading will believe the article was restored.

Nothing recovers automatically. Silently replacing a document with a flattened version of itself would hide the fact that something was lost, which is the failure this whole path exists to stop.

The nightly check

felagi:check --quiet-when-clean runs at 01:00 and says nothing unless something is wrong, because a nightly command that prints on success trains everybody to ignore its output. It writes a warning to the log as well as the terminal: nothing watches a scheduled run, and a problem that only appears on a terminal somebody closed is a problem nobody has.

Not there yet

  • No offsite copy. Retention keeps seven nights on the same disk as the application. That is the floor described above, and it is the largest remaining gap on this page.
  • No consistency snapshot across all three. The database is read inside one transaction, so the relational half is a single instant, and the dump is taken before the archives so it sits closest to the manifest's counts. A file written between the two still lands in one and not the other — for a knowledge base that means an article saved during the backup, which the check will report.
  • One engine, plus the test suite. MySQL 8 is required to run Félagi, and the dumper writes MySQL and SQLite because the test suite runs on SQLite — SQLite is a test target and never a deployment one. Any other connection is refused rather than skipped, and --without-database is how an operator says they are taking the database themselves.
  • Nothing verifies a restore end to end on a schedule. The nightly run proves the file is readable, not that it reconstructs the database. Rehearsing that means restoring into a scratch database, which needs somewhere to put one.