Backups¶
Your data/ directory is the source of truth. Back it up.
The most important file is data/scholar.db. Everything else
(caches, secrets, embedding fetch state) is regenerable from it
plus your settings.json.
What's in data/¶
data/
├── scholar.db # Primary SQLite — papers, authors, settings, etc.
├── scholar.db-wal # WAL — write-ahead log
├── scholar.db-shm # Shared memory — WAL coordination
├── secrets.json # Optional — secrets ALMa wrote
├── backups/ # UI-driven backup snapshots land here
└── caches/ # Embedding / network caches (regenerable)
WAL files (-wal, -shm) are part of the live database. Don't
copy scholar.db while ALMa is running without using the online
backup paths below — you'll get an inconsistent snapshot otherwise.
Online backup (UI)¶
Settings → Data & system → Library maintenance → Backup:
- Create backup — uses SQLite's online backup API. Safe while
ALMa is running. Writes a gzip-compressed, timestamped snapshot to
data/backups/scholar_<timestamp>.db.gz. - List backups — shows existing snapshots with size and timestamp.
- Restore from backup — decompresses the chosen snapshot and
overwrites the live
scholar.dbin place. Restart ALMa afterwards so it reopens the restored file cleanly.
Online backups are atomic at the SQLite page level — no risk of a torn read.
Online backup (API)¶
Same operations via REST:
The backup name is a server-generated timestamp (returned as
backup_name); there is no custom-name parameter.
# create — returns {"success": true, "backup_name": "20260425_143000", ...}
curl -X POST http://localhost:8000/api/v1/library-mgmt/backup
# list — the `backups` array in the info payload
curl http://localhost:8000/api/v1/library-mgmt/info
# restore — pass a returned backup name
curl -X POST http://localhost:8000/api/v1/library-mgmt/restore/20260425_143000
Offline backup (file copy)¶
If ALMa is stopped:
# stop ALMa first
docker compose down # or kill the uvicorn process
# tar everything
tar -czf alma-backup-$(date +%F).tar.gz data/ settings.json .env
# done; restart
docker compose up -d
If ALMa is running, you can still make a consistent file copy by forcing a SQLite checkpoint first:
sqlite3 data/scholar.db "PRAGMA wal_checkpoint(TRUNCATE);"
cp data/scholar.db data/scholar-snapshot.db
The checkpoint folds the WAL back into the main file, then the copy is consistent. Repeat as a cron job for periodic offline backups.
Restore¶
For a UI-driven restore, see above.
For a file-copy restore:
- Stop ALMa.
- Replace
data/scholar.db(and remove any-wal/-shm). - Start ALMa.
- Watch the Activity panel — the schema migration check runs on start-up. Fresh backups don't need migration; older ones might.
Verifying a backup¶
A quick sanity check after restore:
sqlite3 data/scholar.db "SELECT COUNT(*) AS papers FROM papers;"
sqlite3 data/scholar.db "SELECT COUNT(*) AS saved FROM papers WHERE status='library';"
sqlite3 data/scholar.db "SELECT COUNT(*) AS authors FROM authors;"
Compare against the numbers you had before. The Insights overview also shows totals — open it after restore and confirm.
Export¶
Different from backup — exports are lossy but portable:
- JSON export —
GET /api/v1/backup/export/jsondumps the saved Library as JSON. Useful for moving to another tool. - BibTeX export —
GET /api/v1/backup/export/bibtexdumps saved papers as.bib. Good for citation managers.
These don't preserve internal state (lens configs, signal events, schedules). For a lossless backup, use the file or online paths above.
Cadence¶
A reasonable default for a personal install:
- Online backup: weekly (cron the API call).
- File-copy backup: monthly to off-machine storage (cloud bucket, second drive).
- Before any risky operation: manual backup right before Settings → Library maintenance → Reset / dedup / large bulk import.
What's not backed up by default¶
frontend/dist/— regenerable withnpm run build.__pycache__/,.venv/— regenerable withpip install -e ..data/caches/— regenerable on next refresh.
The full backup target is data/ + settings.json + .env. Keep
them on a backup that includes file timestamps; ALMa uses
added_at / updated_at heavily, but those are SQLite columns,
not filesystem timestamps.