0029: App data in SQLite
On this page
Status: accepted. Its row for requests and dumps is replaced by 0052: the Monitor keeps them on disk, masked.
Context
Everything high-volume lived in leashd's memory and was lost on every restart, including every update. For most of it that's fine, but a webhook delivery you held to release later was silently dropped. Task lists and terminal details were whole JSON files rewritten on every change, under one process-wide lock, with no cap. The secrets files were written in place, so a crash part way through a save could quietly reset the Cloudflare tokens and the share password.
Decision
-
Place each kind of data by what it needs.
Data Where Kept Webhook deliveries, held ones included Database; bodies over 64 KiB as files in db/blobs/, named by their SHA-256Last 500 per webhook, for the days chosen in Settings › Webhooks (14 by default, 1 to 365); held ones until released Task lists Database Up to 1,000 tasks per site Terminal details Database; their output stays in terminals/<id>.outThe newest 200 terminals Scratchpads and their last runs' code (added by 0030); what a run produced stays in memory Database 100 per site, the last 50 runs of each Review comments on a site's changes ( change_comments, added by 0080)Database Open ones until resolved or the site is removed (90 days after the newest one when the site is gone but wasn't removed); resolved ones for the days chosen in Settings › Models › More (30 by default, 1 to 3650) Notes with their tags, links and full-text index (added by 0087); pictures pasted into them as files in db/note-images/, named by their SHA-256Database and files Notes until deleted; a picture no note shows, 7 days after the last note stopped showing it Traffic per site per minute Database: counts, 5xx, p95, median, slow queries. Never a header, path or body 24 hours Requests, dumps, process logs, notifications Memory, as before Until Short Leash restarts config.toml,secrets/*.json, the CA and certificatesFiles, as before Requests and dumps hold cookies, tokens and bodies, and the docs promise that requests are never written to disk. Notifications stay in memory as 0027 decided; the one that matters after a restart, a held delivery, is raised again from the database. Settings stay a file you can edit, and secrets stay out of the database (0012).
-
rusqlite with SQLite bundled (MIT, public domain), and rusqlite_migration (Apache-2.0) for migrations. The MSRV goes to 1.95 for rusqlite_migration. sqlx and SeaORM bring an async framework and a query macro for five tables; redb and fjall have no SQL and no inspection tool like
sqlite3; turso isn't 1.0 yet and uses the same file format, so a later switch stays open. -
One file, one writer.
<home>/db/leash.db, in a folder only you can open (0700), with the database, its write-ahead log and its copies readable only by you (0600). Only leashd opens it. The app, the CLI and AI tools go through leashd's API. One connection lives on its own thread: every call is a job run in order, writes useBEGIN IMMEDIATE, and no read is held across an.await. WAL withsynchronous=NORMALloses at most the last commits to a power cut and nothing to a crash. -
Retention on every table, applied when rows are written and by upkeep every ten minutes, which also runs
incremental_vacuumandwal_checkpoint(TRUNCATE).optimizeruns hourly. A fullVACUUMnever runs. -
Recovery. At open,
quick_checkruns. A damaged file is moved toleash.db.corrupt-<time>with its sidecar files (and its webhook bodies linked intoleash.db.corrupt-<time>-blobs/), and the daily copy (leash.db.bak-daily, made withVACUUM INTO) is restored, or a new database started. A notification names the moved file. Migrations take aleash.db.bak-v<N>copy first. A database written by a newer Short Leash is left alone. -
Short Leash never fails to start because of the database. When it can't be opened, leashd runs as before it had one: deliveries in memory, tasks and terminals in their files. A notification says what failed and what to do.
-
Updating from a version without a database imports
tasks/*.jsonandterminals/terminals.jsoninside the transaction of the migration that creates their tables, and moves the files aside (tasks.imported/,terminals.json.imported) only after it commits. A file that can't be read is left in place and named. Rows go in withOR IGNORE, so files left behind are imported at the next start. -
Secrets are written atomically.
fsx::write_private_atomicwrites a 0600 temporary file, flushes it, renames it over the old one and flushes the folder. A secrets file that can't be parsed is moved to*.unreadable-<time>and named in a notification, never quietly treated as empty.
Consequences
- Held deliveries, tasks, terminal details and the Overview's last hour survive restarts and updates.
- The app adds about 1.7 MB and a C compile of SQLite. The Mac-hosted Windows clippy check sends
libsqlite3-sysdown its linked path so it compiles no C; real Windows CI builds the bundled SQLite. - Webhook bodies and terminal details are now on disk. They're readable only by you, like terminals' saved output. Request content still never is.
- Reinstalling an older version shows no tasks, because their files were moved; renaming
tasks.imported/back totasks/brings them back. Deliveries held in an older daemon's memory can't be saved when it updates. - Every new table needs a retention rule in its first migration, and every change to a table is a new migration.