Data
Export and backup
Seven export routes, what each is for, the CSV conventions, how to back up and restore an instance, and the one warning that matters.
Your data is one SQLite file and one media folder. Export gives you readable copies of both; backup is your job, and it is a small one.
The full export is an unencrypted copy of every reflection and check-in
It is the copy you restore from. It is never the copy you share. Share stamped photos and
recap cards โ those are built by whitelist. The export page and the bundle's MANIFEST.txt
both say this in one line, on purpose.
The routes
All under /export, all behind whatever authentication your reverse proxy provides, none on
the API. Filenames are latch-<what>-YYYYMMDD-HHMM.<ext> in your local time.
| Route | What it is |
|---|---|
/export |
the page, with size estimates |
/export/latch.json |
every table, verbatim โ the only authoritative copy, the one to restore from |
/export/daily.csv |
one row per local day, gaps filled with zeros โ the one to open in a spreadsheet |
/export/events.csv |
one row per event; undone rows are kept and marked with voided_at |
/export/sessions.csv |
one row per lock, clock recomputed from the log |
/export/entries.csv |
reflections and check-ins, the five scores as their own columns |
/export/assignments.csv |
every training assignment |
/export/kit.csv |
one row per thing you own: specs, cost, bouts, hours, cost per bout |
/export/bouts.csv |
every bout of play or wear โ what was on, how long, caged or not, how it ended |
/export/bundle.zip |
all of the above plus media/ (originals and thumbnails) and MANIFEST.txt, streamed. ?media=0 for data only |
CSV conventions
- Every timestamp appears twice: the epoch column is the stored truth; the
*_localcolumn (YYYY-MM-DD HH:MM:SS, no offset, zone in thetzcolumn) is what a spreadsheet parses. - An empty score cell means no measurement was taken. It does not mean zero โ the scale starts at 1.
- Files carry a UTF-8 BOM so Excel reads them correctly. Python readers want
encoding="utf-8-sig". - Voided rows stay in
events.csvwith theirvoided_at, because the log is append-only and the export is the log.
Backing up an instance
The container writes to one volume (/data). A backup is that volume, taken WAL-safely:
# the database โ never `cp` a live SQLite file; the WAL may hold hours of writes
docker exec latch python -c "import sqlite3; s=sqlite3.connect('/data/latch.db'); d=sqlite3.connect('/tmp/latch.backup.db'); s.backup(d); d.close()"
docker cp latch:/tmp/latch.backup.db ./latch-$(date +%F).db
# the media โ plain files, copy them however you like
docker cp latch:/data/media ./media-$(date +%F)
Or simply download /export/bundle.zip on a schedule you will actually keep. Verify a backup
by a content watermark, never by file size:
SELECT COUNT(*), MAX(ts) FROM events;
The same two numbers on the live database and the copy mean the copy is current.
Restoring
- Start a fresh instance (empty volume) so the schema exists.
- Stop it. Replace
/data/latch.dbwith your backup (or importlatch.jsontable by table with foreign keys off โevents.voided_bypoints forward to a later id, so an id-ordered insert with them on fails partway; the JSON lists tables in a safe order). - Copy
media/back under/data/. Thumbnails are optional: a missing one is rebuilt from the original on demand. - Start it.
/healthzreports the version and the session state;/progressshould read as it did.
Run PRAGMA foreign_key_check after a JSON import. A media row whose bytes are gone shows in
the gallery as a refusal, not a crash.
Where the data is
| Container | Dev on a workstation | |
|---|---|---|
| database | /data/latch.db (WAL mode) |
./.data/latch.db |
| media | /data/media/YYYY/MM/ + media/thumbs/ |
./.data/media/ |
| settings | rows in the settings table |
same |
Nothing is stored anywhere else.
This page also ships inside the app, at /guide โ so your own instance always serves the guide for the version you are running, with the internet unplugged. Get Latch ยท Something wrong here? Tell me.