Skip to main content

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:

HeaderExample
X-Log-Lens-TokenX-Log-Lens-Token: your-secret
Authorization: BearerAuthorization: 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.