Operate

Security

What protects your diary when a key is in play, described as it is built. The code is open source if you want to check.

How keys are stored

Tally stores only the SHA-256 hash of each full key, plus its id and last four characters for the list. The key itself is shown once, when you create it, and never again. The secret part is 256 random bits, so a hash that leaked could not be turned back into a working key by guessing. If you lose a key, revoke it and make a new one.

Scopes

A key is either read only or read & write, and the check runs on every request. On the REST API, a read-only key may only use routes that read. By default those are GET, HEAD and OPTIONS; two GETs that can change stored data need write, and one POST that saves nothing needs only read. Anything else is refused with 403. On the MCP server, a read-only key is served a separate tool set that has no write tools at all, so there is nothing to call by mistake. Scopes in detail.

Accounts stay apart

Every request runs as one account inside Postgres row-level security. The app connects as a database role that cannot bypass it, so a query only ever sees that account's rows, whatever the code asks for. Your diary, logs, saved meals, settings, weigh-ins and API keys are all under that rule; the food records they point to are a shared cache. Another account's ids simply read as 404.

One step has to happen before the account is known: finding whose key was presented. That is a single narrow database function. Given a key id and a hash, it answers only “whose live key is this?”, and only when both match and the key is neither revoked nor expired. The right to run it is withdrawn from the public and granted only to the app's database role. It also reads the account's current email, so an email change applies from the next request.

Revocation and expiry

Revoked and expired keys are checked on every request, with no cache in between, so a revoked key fails on its very next call. Revoked, expired, unknown and malformed keys all get the same 401 invalid API key, so the answer never reveals which keys exist. A revoked key stays in your list, dimmed, as a record. An account keeps at most 50 keys, so a new key past that deletes the oldest revoked or expired ones.

Keys cannot manage keys

Creating, listing and revoking keys needs a signed-in app session. A key presented to /api/keys gets 403. So a leaked key cannot mint a replacement for itself, cannot see your other keys, and cannot undo its own revocation.

Browsers

The API sends no CORS headers, so a web page on another site cannot read its responses. A signed-in app session is also held to the app's own origin: a state-changing request with a session token and another site's Origin header is refused with 403 origin mismatch. Requests with an API key are not checked for Origin. The key is the whole credential and a browser never adds it to a request by itself, so another site has nothing to forge, and a client that runs in a browser engine, such as an extension, can use a key. Keys belong in servers, scripts and agent configs, never in a web page, where anyone could read them.

Good practice

  • Give an agent that only answers questions a read-only key.
  • Set an expiry on keys for experiments; 30 days is plenty for trying a client.
  • Use one key per client, labelled with where it lives, so revoking one does not break the rest.
  • Keep keys in environment variables or a secret store. Never commit them, and never paste them into a chat or an issue.
  • If a key may have leaked, revoke it in Settings first, then make a new one.