API key authentication
Log Lens is local-first, so out of the box its JSON API is unauthenticated - anything that can reach the app can query and change your data. That is fine on a private machine, but the moment Log Lens is exposed to a network you should require an API key. Authentication is opt-in: you turn it on by setting a single secret.
Turning it on
Set LOG_LENS_TOKEN in a real environment variable or in a root-level .env file:
LOG_LENS_TOKEN=your-long-random-secret
- Empty (the default) to the API is unauthenticated.
- Any non-empty value to every API request must present that exact token.
A real environment variable always wins over the same key in .env, so .env is just a convenience for setups (such as Herd) where exporting variables into PHP-FPM is awkward. The value is trimmed, so surrounding whitespace is ignored.
Presenting the token
A client may send the token in either of two headers:
| Header | Example |
|---|---|
X-Log-Lens-Token | X-Log-Lens-Token: your-secret |
Authorization: Bearer | Authorization: Bearer your-secret |
X-Log-Lens-Token is checked first; if it is absent, Log Lens reads a Bearer token from the Authorization header.
curl 'http://127.0.0.1:8787/?api=issues' \
-H "X-Log-Lens-Token: your-secret"
What a rejected request looks like
If a token is configured and the request presents none, or the wrong one, Log Lens returns 401 and does not run the request:
{ "error": "A valid API key is required. Send it as X-Log-Lens-Token or Authorization: Bearer." }
The comparison uses hash_equals, a constant-time check, so it does not leak the token through timing differences.
Good to know
- Authentication applies to every API request - reads and writes alike, safe methods included.
- The bundled Claude/Codex skills read the same
LOG_LENS_TOKEN, so setting it keeps them working without extra configuration. - This key is separate from Log Lens's cross-origin protection, which blocks state-changing browser requests from other origins. The two run together.