Latch

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.

On this page

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

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

  1. Start a fresh instance (empty volume) so the schema exists.
  2. Stop it. Replace /data/latch.db with your backup (or import latch.json table by table with foreign keys off โ€” events.voided_by points forward to a later id, so an id-ordered insert with them on fails partway; the JSON lists tables in a safe order).
  3. Copy media/ back under /data/. Thumbnails are optional: a missing one is rebuilt from the original on demand.
  4. Start it. /healthz reports the version and the session state; /progress should 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.