Guide βΊ Self-hosting
Hosting it for other people
Hosted mode β one instance, many profiles, each in its own database behind a login. What the host can and cannot see, how invites work, the admin commands, and why the JSON API is off.
Latch is a solo game and its default install is one person, one box, no login. Hosted mode is for the other case: you run one instance and a handful of people you know each get a profile on it β a beta round, a pack, a friend who will never run Docker. Nothing about the game changes. What changes is who is standing at the door.
Read this if you are a player on someone else's instance
Your log, reflections, photos and voice memos live on the host's machine, in a plain database file the host could open. Latch does not encrypt them at rest. The host also has no way to read your password (it is hashed) and no way to sign in as you without resetting it β but the data itself is theirs to lose, back up, or look at. Play on a hosted instance only with someone you would hand a shoebox of photos to. You can export everything and delete your profile yourself from Account, at any time, and deletion removes the whole directory.
How it works
Turn it on with one variable:
environment:
LATCH_HOSTED: "1"
Each profile is a directory under the data volume β profiles/<id>/latch.db plus its
media/ β and a request only ever sees the directory of the profile whose session cookie it
carries. There is no shared table with a profile_id column that a query could forget to
filter on; isolation is a property of the filesystem. A small registry, hosted.db, holds
handles, password hashes, invite codes and sessions, and nothing else.
Every profile's database is migrated and seeded when the instance starts, so an upgrade reaches the pup who has not signed in since β and the background clock ticks every enabled profile on the same schedule it always did, so overdue training is stamped at its deadline, not at their next visit.
Say what this instance is, in your own words
LATCH_HOSTED_NOTICE: "Beta round one. I can see whatever you submit. Everything here is destroyed on 31 October."
That sentence renders on the register page above the acknowledgement tick, and again on the sign-in page. The generic paragraph (a person, not a company; the host can read the data) always renders; this is where you say who you are, how long the instance lasts and what happens to the data at the end. Two sentences that are true beat a policy page.
Doors
| Page | Who |
|---|---|
/login |
anyone with a handle and password |
/register |
anyone holding an unused invite code β there is no open sign-up |
/guide, /healthz, /static/β¦ |
public: the manual, the health probe, the stylesheet |
| everything else | a signed-in profile |
Sessions are a random token in an HttpOnly, SameSite=Lax cookie, stored hashed, valid
30 days and extended as you play. Changing your password signs out every other device.
Eight wrong passwords in a row lock a handle for ten minutes β per handle, not per address,
because behind a reverse proxy every visitor can look like the proxy.
Calendar feeds work, per profile, and one rule is stricter here: a feed URL must resolve
to a public address. A tester who could make the server fetch http://10.0.0.1/β¦ could map the
host's own network from inside it, so private, loopback and link-local destinations are refused
β on the URL and on every redirect. The .ics file import needs no network and is unaffected.
The JSON API is off on a hosted instance, always, whatever LATCH_API_TOKEN says. The
bearer token is one global secret; on a box with many profiles it would open all of them at
once. Shortcuts and the health sync are single-player features until the API grows
per-profile tokens.
Running it: the admin commands
There is no admin web page. A hosted beta needs an admin about as often as it needs an invite minted, and an admin login is a second thing to get right on an app that just grew its first. Everything is a command inside the container:
docker compose exec latch python -m latch.hosted invite --note "for kai" # prints XXXX-XXXX-XXXX
docker compose exec latch python -m latch.hosted invites # who used which
docker compose exec latch python -m latch.hosted list # profiles, last seen, sessions
docker compose exec latch python -m latch.hosted disable <handle> # out on their next click
docker compose exec latch python -m latch.hosted enable <handle>
docker compose exec latch python -m latch.hosted passwd <handle> # the only "forgot my password" path
docker compose exec latch python -m latch.hosted delete <handle> --yes # the whole directory, gone
Invites are single-use and expire after 14 days by default (--days 0 for never). Hand the
code over however you already talk to that person; the register page takes it in any case
with or without the dashes.
There is deliberately no email anywhere in this: no verification, no reset links, no
notifications. A mailer is an outbound call, a credential to hold, and a way for the
instance to be turned into a spam relay. Password resets are you, running passwd.
What the host should also know
- Back up
profiles/andhosted.dbtogether. A profile directory without its registry row is a directory nobody can sign in to; a row without its directory is an empty game. Both are WAL-mode SQLite β copy them withsqlite3 .backupor a stopped container, never a barecpof a live file. - Disk. Each verification photo is stored once (up to 40 MB) plus a thumbnail; budget a few gigabytes per active player and watch the volume.
- The register page says, in the player's own words, what you can see. Do not remove that paragraph. Someone who reads it and signs up anyway has made an informed choice; someone who did not get to read it has not.
- Timezone is per profile, chosen at sign-up and editable under Account. Days, streaks and the Handler's evening check all roll over on the player's own clock, not the server's.
- Everything in Install and self-host about HTTPS still applies, and more so:
the camera and microphone need a secure origin, and so does the
Secureflag on the session cookie (set automatically when the request arrives over HTTPS).
What it is not
Hosted mode is players side by side, each alone in their own game. Nobody can see anyone else's clock, log or photos, including the host through the app. It is not the packs proposal β a Handler who can act on several pups' clocks β and it is not a social layer. Those remain separate designs with separate rules.
This page is docs/hosted.md in the repo. Spotted a mistake? Tell us how to report it β