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, why the JSON API is off, and the open demo.
On this page
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.
There is no AI Handler on a hosted instance. It talks to a model on the pup's own computer or network, and a hosted box's network is the host's, not yours โ so its guard refuses every call there, and its page says so.
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).
Demo mode
After a beta round the same instance can stay up as a trial run anyone can click around in โ no invite, no account, nothing kept. Turn it on with one more variable:
environment:
LATCH_HOSTED: "1"
LATCH_DEMO: "1"
# optional:
LATCH_DEMO_HOURS: "24" # how long a demo kennel lives (1โ168)
LATCH_DEMO_MAX: "40" # demo kennels at once
LATCH_DEMO_PER_HOUR: "30" # new ones in any hour
A visitor with no session lands on /demo: what Latch is, three plain rules, an
I am 18 or over tick and one button. Pressing it makes them a throwaway kennel โ a
profile with a random handle (pup_3fa9c1), no password, and about five months of invented
play already in it, ending now โ and signs them in with a cookie. Every page then carries a
banner saying when it will be deleted, and Account becomes delete it now plus how to
install Latch for real.
| On a demo instance | |
|---|---|
/demo, /demo/start |
the door and its button โ the only way in |
/login, /register, /feedback |
gone (they send you to /demo) |
| uploads | refused โ photos, voice memos, playset and pup-file imports, all of them |
| calendar feeds | never fetched |
| the JSON API, the AI Handler | off, as on any hosted instance |
| a demo kennel | deleted LATCH_DEMO_HOURS after it was made, and on every restart |
Why it works this way
Each of these is a choice, and each has a reason:
- It refuses to start on a box that holds invited profiles. The demo door is open to
anyone on the internet. A box that still holds a tester's journal and photos is the wrong
box to open it on, however well the profiles are separated. When the beta is over, have
testers export, delete their profiles (
python -m latch.hosted delete <handle> --yes), then turn the demo on. It also refuses withoutLATCH_HOSTED: a demo is a policy on top of hosted mode, never a single-player box opened to strangers. - A kennel is made by pressing a button, never by visiting a page. A crawler, a chat app's link preview or a browser's prefetch would otherwise make a database on every request.
- 18+ is a tick on the door. The invented log is about chastity and pup play; nobody should land in it without being told.
- No uploads, refused at one door. Every
multipartPOST is turned away before any route reads a byte of it, so a stranger's photos and recordings are never held, not even for a day โ and an upload form added later cannot be the one that forgot. Refusing was simpler than accepting and sweeping, and it leaves nothing to sweep. - No calendar feeds. A stranger's calendar URL is not one this server should fetch on a timer.
- No feedback pages. On a beta, feedback outlives the profile on purpose. A stranger's words should not.
- No password, no login. The cookie is the only key. A demo kennel that could be signed into again would be an account, and accounts are what the demo is meant to avoid.
- A fixed lifetime from creation, not from last use, so a kennel kept open in a tab cannot live forever. A restart takes everything: nothing persists includes a reboot.
- Caps on live kennels and on new ones per hour, never per address. Behind a reverse proxy every visitor looks like the proxy, so the only honest limits are global ones. A visitor who already has a kennel is sent back to it rather than given another.
- Each kennel is seeded fresh, not copied from a template. A template would be stale the
day after it was made (its lock ends, its "today" is yesterday). Seeding takes about a second
because the generated photos โ the slow part โ are made once and kept under
demo-photos/on the volume; the first boot spends ~15 seconds making them in the background.
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, where a Handler can act on several pups' clocks โ and it is not a social layer. Those remain separate designs with separate rules.
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.