Data
Privacy and what never leaves
The exact boundaries โ no egress, no accounts, prose versus facts, what a share card can and cannot carry โ and the tests that keep them true.
On this page
Privacy is the first principle here, and the rest of the design follows from it. This page is the list of what that means in practice, so you can check it rather than take it on faith.
Nothing leaves the box
- The core makes zero outbound calls. The app itself contacts nothing. No update check, no analytics, no crash reporter, no font, no script, no icon from a CDN. This guide is rendered on your instance from markdown in the repository.
- A module may reach the network โ only if it declares it. The core is sealed; optional modules (posting a share card, fetching a playset) are where anything outbound can live. Such a module declares its egress and the app tells you before you enable it, so the choice is optional and informed. This is honesty, not a wall: the app can say a module reaches the network โ it cannot promise one never could. Class-P data never rides along either way โ health, location, journal and the rest stay on the box; a module's egress is for things like a card you chose to post, never your log.
- No accounts. There is no sign-up, no server of ours, no "us". One instance, one player.
- No Swagger page. FastAPI's
/docsloads its UI from a CDN and a favicon from a project site โ a beacon on every visit. It is disabled, along with/openapi.json(which would publish the route inventory to anyone without a token). The API is documented here instead. - Enforced by a test.
tests/test_flow.py::test_no_page_references_an_off_box_originscans every served template and static file for anhttp(s)://host and fails on anything that is not a namespace identifier or an error-message string. Add a CDN font and the suite goes red.
Sharing is explicit egress
The only things that ever leave are files you exported and chose to send:
- Stamped photos, recap cards and share cards are re-encoded JPEGs. Their EXIF is gone because they are rendered from pixels โ the original metadata is never copied across, so camera model, serial and original GPS cannot ride along.
- Location is the one exception, and it is deliberate. If you ask for it, a card can
print a coarse place โ by default a ~1 km cell. That coordinate was rounded when the
photo was saved, so the exact position is not in the database, the backups or the
export bundle, and nobody who gets a copy can sharpen it. Recording it is a setting
(
photo.place_capture); printing it is off unless that specific export asks. See Location on photos โ including what rounding does not buy you. - The phone's share sheet is the browser's Web Share API handing your file to another app. Latch itself posts nowhere.
- A future share layer (posting a card to your own fediverse account, say) is designed as an optional module with per-rule consent โ never a default, never integral. See the roadmap.
Prose versus facts
Every design decision about what can be seen where comes down to one line: facts are shareable; prose is yours.
| Examples | Share cards | /api/v1 event feed |
Export | |
|---|---|---|---|---|
| Facts | how long, day N, streak, tasks done, rank, which photo, which task | yes | yes | yes |
| Prose | reflection text and excerpts, check-in scores and notes, session notes, adjustment reasons, photo captions | never | never | yes |
Concretely:
/api/v1/eventsreturns a whitelisted payload per event (ids, flags, durations, catalogue labels). The stored payload carries a reflection excerpt and check-in scores; the API does not./api/v1/handler/todaysays what is due, never what was written./api/v1/stateomits the session note./api/v1/handler/entriesis the one deliberate exception: it returns reflection and check-in bodies, because a local bot that reads your journal is a designed use. That single route is why the API token is a LAN-only secret โ it belongs in a password manager and never in anything that leaves the house.- Export lives outside the API entirely, behind the reverse proxy's authentication. The
bearer token โ designed to sit in a phone Shortcut โ cannot pull your whole journal in one
request. A test asserts every
/api/v1route against a written allowlist; a new route fails the suite until someone writes down what it exposes. - Recap cards do not print the session note unless the URL says
?note=1. tests/test_api_privacy.pyseeds marker strings into prose fields and greps the raw bytes of every API response for them.
The hidden sentence
With a hidden timer, started + sentence = end. The start time is on the dashboard, so the
sentence is the secret: it is withheld from the timer, the log rows, the stats table, the
overlay tokens and the emergency-key confirmation until the lock ends. Adjustments and frozen
time stay visible โ breaking one operand is enough.
The container
read_only: true, no-new-privileges, a 512 MB memory limit, an unprivileged user, a tmpfs
/tmp. Uploads are capped on the stream (before bytes are read), not on the result, so an
oversize POST costs a refusal and not memory. There is no shell tool in the image that could
reach out.
What you are responsible for
- Put it behind authentication. Latch has no login of its own. A reverse proxy with an auth layer, or a private network, is required โ see Install.
- Do not expose it to the internet. Nothing in it is designed for that.
- Encrypt your backups at rest if the disk they land on is not already encrypted. The export is plaintext by design.
- Keep the API token private. It can read your journal.
- Screenshots carry what is on screen. Before you send one to report a bug, look at it.
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.