Self-hosting
Troubleshooting
The messages you will actually see, what each one means, and the fix โ most of them are the game refusing something on purpose.
On this page
- Camera, microphone or share sheet does nothing
- "Photo is over 40 MB" / "Recording is over 12 MB"
- HEIC
- "That's a video โ Latch stores photos, not clips"
- "That's N characters. Your Handler wants at least 120."
- "That recording didn't say how long it is"
- "Wheel is cooling down โ 42m to go"
- "Lock up first" / "The session is frozen"
- "That's before this lock started" / "overlaps the previous lock" / "Not frozen at that time"
- Undo refused
- "This task needs a verification photo"
- The timer reads ??:??:??
- A row says "โถ logged later"
- The Handler says a reflection was missed and it wasn't
- "This box does not answer to that name"
- "Cross-site request refused" or "This page expired"
- API answers 503
- 502 from the reverse proxy
- "That file is not in the volume"
- Times are off by hours
- Logs
- Still stuck?
Most "errors" in Latch are refusals with a sentence attached. The sentence is the diagnosis. The ones people ask about:
Camera, microphone or share sheet does nothing
Cause: you are on an insecure origin (http://โฆ:8770). Browsers do not expose
navigator.mediaDevices or the Web Share API there. Fix: use your https:// address through
the reverse proxy. Do not widen the firewall or terminate TLS on the app container to get
around it โ the proxy is the door. See Install โ HTTPS.
"Photo is over 40 MB" / "Recording is over 12 MB"
The upload ceilings, checked on the stream before bytes are read. Resize the photo; keep memos under ten minutes (the recorder stops itself there).
HEIC
The browser converts these for you now. When you pick a HEIC, Latch spots it and turns it into a JPEG before uploading โ on an iPhone that just works, because iOS can decode its own format. You will see "Converted to JPEG" under the button.
Two things worth knowing:
- Conversion loses the photo's metadata, because it goes through a canvas. The original capture time and any location in the file do not survive it, so the check-in is recorded as now and the location comes from the phone instead (if you have that turned on). For a photo you are logging from weeks ago, set the time yourself with taken earlier.
- A desktop browser usually cannot decode HEIC. Chrome and Firefox on a computer will say "this browser can't convert it here" and send the file as-is, and the server will refuse it with the fix below.
If you get "That's an iPhone HEIC photo, which this server cannot decode": set the camera to Settings โ Camera โ Formats โ Most Compatible (which affects new photos only), add Convert Image โ JPEG to the Shortcut, or export the photo as JPEG from the Photos app.
Latch converts only formats it cannot otherwise read. A JPEG or PNG is uploaded untouched, which is deliberate โ a canvas round-trip would strip the metadata off photos that are working perfectly well.
"That's a video โ Latch stores photos, not clips"
Pick a video in a photo field and Latch offers you a button: ๐ท use a frame from it. One tap pulls a still out of the clip and logs that.
It is an offer rather than something that just happens, because it is not a lossless conversion the way HEIC โ JPEG is โ the motion and the sound are exactly what someone filmed a clip for, and quietly swapping in a still would be the app substituting a different thing for the one you chose.
If the browser cannot decode the clip you get "this browser can't decode that clip" โ take a still on the phone instead. Storing video itself is a separate question; see the roadmap.
Every other refusal names whatever you actually sent โ a .mov, a PDF and a TIFF each get
their own answer rather than being told to convert a HEIC they never uploaded.
"That's N characters. Your Handler wants at least 120."
The reflection minimum. Write more, or lower handler.reflection_min_chars on
Settings. Same for "That's N seconds" on a spoken one.
"That recording didn't say how long it is"
The browser sent no duration and the file's format could not be measured. Happens with some
third-party recorders posting raw audio to the API. Send duration_seconds, or record in the
app (which always sends it), or set handler.voice_min_seconds to 0 to accept unmeasured memos.
"Wheel is cooling down โ 42m to go"
The cooldown, 60 minutes by default. Set it to 0 on Settings while testing.
"Lock up first" / "The session is frozen"
The wheel spins only on a locked, unfrozen session.
"That's before this lock started" / "overlaps the previous lock" / "Not frozen at that time"
The retroactive log's guards. A clock event cannot precede the lock it lands in; a backdated lock cannot overlap the previous one; an unfreeze cannot precede its freeze. Check the when you typed. See the retroactive log.
Undo refused
The button names the reason: outside the window, or an event kind that carries side effects (training, a photo, an unfreeze, the lock itself). See Undo.
"This task needs a verification photo"
That is what requires verification means. Attach one, or edit the task in the library.
The timer reads ??:??:??
A hidden lock. You chose to see only elapsed time. It resolves when the lock ends.
A row says "โถ logged later"
You (or the scheduler, after downtime) recorded something more than two minutes after it happened. Correct and permanent โ the record stays honest about when it was told.
The Handler says a reflection was missed and it wasn't
The audit checks yesterday's local day for a non-voided reflection while you were locked at 20:00. If you wrote it but undid it, or wrote it after midnight without a when, it counts as missed. Undo the penalty within the window, and backdate the reflection with when.
"This box does not answer to that name"
You opened Latch by a name it has not been told about. Either:
- open it by its IP address or
localhost(those always work), go to Settings โ System โ answers to, type the name and press Answer to this name; or - put the name in
LATCH_ALLOWED_HOSTSand recreate the container.
A name is never learnt just by opening it โ not even the first time. See the names it answers to.
Behind a reverse proxy, this on every page usually means the proxy hands Latch an
upstream name instead of yours (proxy_pass http://latch:8000; sends Host: latch:8000):
pass Host through (nginx: proxy_set_header Host $host;). Through a proxy that sets
X-Forwarded-Host, every name in that header must be allowed too.
If Settings โ System lists entries as ignored, LATCH_ALLOWED_HOSTS holds something that
can never match โ a URL (https://โฆ), a bare *, a trailing slash. Write bare names:
latch.home.example, *.home.example.
"Cross-site request refused" or "This page expired"
A form was sent from another site, or a page was left open too long (form tokens last 30 minutes and do not survive a restart). Reload the page and try again. Nothing was changed.
If it happens on every form behind a reverse proxy, the proxy is rewriting the Host
header to its upstream address (nginx does this by default), so the browser's Origin can
never match it. Pass the original Host through (nginx: proxy_set_header Host $host;) or
set X-Forwarded-Host โ see behind a reverse proxy.
Until then localhost or the box's IP address works, and so does the emergency key without
a browser: curl -d force=1 <address>/session/unlock.
If it happens on every form and you are not behind such a proxy, your browser (a privacy
setting or an extension) is forcing no-referrer, which makes it send Origin: null on the
app's own forms. Latch accepts that only where the browser also vouches with
Sec-Fetch-Site, which browsers send only over HTTPS and to localhost โ and only on a
name, never on a bare IP address. Over plain http:// to a LAN address or name every form
is refused. Use HTTPS (your proxy) or http://localhost:8770, allow referrers for this
site, or use curl for the emergency key.
API answers 503
LATCH_API_TOKEN is empty. That is the API being off on purpose. Set it and recreate the
container. 401 is a wrong or missing bearer.
502 from the reverse proxy
The container is down or restarting. docker compose ps, then docker compose logs latch.
/healthz on the raw port from the proxy host tells you whether the app is up.
"That file is not in the volume"
A media row survived a restore but its bytes did not โ the database and media/ were restored
from different moments. Copy the media folder from the same backup as the database.
Times are off by hours
LATCH_TZ (and TZ) on the container do not match where you live. Events are stored as UTC
and only displayed in LATCH_TZ, so changing it is safe and instant.
Logs
docker compose logs -f latch. Startup prints the version, the data directory, the modules
found, and whether the API is on.
Still stuck?
Report it with the version from /healthz, what you did, what you saw,
and the exact sentence on screen. Do not attach an export.
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.