Latch

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

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:

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.

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.