Security
Authentication
Section titled “Authentication”- Every non-public route requires a Bearer JWT. Missing or invalid means
401, before any handler runs. - Tokens are signed with
AUTH_JWT_SECRET(HS256), and the algorithm is pinned — analg: nonetoken is rejected. - Signatures are verified in constant time, before the payload is parsed.
- Default lifetime is 7 days. There is no refresh token and no server-side session: expiry means signing in again.
- Passwords are hashed with a memory-hard algorithm and a per-user salt. The hash never leaves the server and is never returned by any route.
- Password reset tokens are short-lived and single-use; changing a password invalidates any outstanding link.
Authorisation
Section titled “Authorisation”Two roles:
| Role | Scope |
|---|---|
user |
Their own watches, runs, feedback, trust rules and devices |
admin |
The above, plus key and model configuration |
Ownership is enforced on every read and write. Another owner’s resource returns
404, not 403, so the API does not confirm that it exists. When creating or
updating a watch, the owner comes from your token — an ownerId in the body is
ignored.
Secret handling
Section titled “Secret handling”- Secrets enter in one place, validated at startup. Nothing else reads the environment.
- Stored API keys are encrypted at rest with authenticated encryption; the master key exists only in the environment, never in the database.
- Keys are decrypted in memory at the point of use, and the API returns only masked previews.
- Every key-configuration write is audited with the masked preview, never the value.
- Watcher never logs a secret. Provider failures are reported by status code.
What an instance exposes
Section titled “What an instance exposes”| Endpoint | Public? | Reveals |
|---|---|---|
/health |
Yes | That an instance exists |
/config/status |
Yes | Which integrations are configured, as booleans — never values |
/auth/register |
Yes | Open self-signup is enabled by default |
/auth/login, /auth/forgot-password, reset and verify |
Yes | Nothing about which accounts exist |
| Everything else | No | — |
Rate limiting
Section titled “Rate limiting”In-memory fixed windows, per process: 20/minute per IP on auth routes, 10/minute per owner on AI routes, and 10/minute overall on key-probing admin routes so the endpoint cannot be used to test stolen keys at speed. Windows reset on restart, and are per instance rather than shared.
Untrusted input
Section titled “Untrusted input”Collected page content is treated as data, never instructions. The evaluator is explicitly told to ignore text inside a page that tries to change its task or claim something is confirmed. The real defences are structural: the quote must exist in the collected text, the link must be one Watcher fetched, and an ungraded domain cannot produce a confirmed alert. See Accuracy.
Data flow and privacy
Section titled “Data flow and privacy”Content Watcher collects is sent to the model provider you configured, along with your condition. Nothing else leaves your infrastructure except the alerts you asked for.
Free second-opinion tiers may use submitted prompts for training. For a watch
over private sources, set allowSecondaryModel: false — it then runs
primary-only, with no second opinion and no failover.
Transport and CORS
Section titled “Transport and CORS”Terminate TLS at your host. The API sends CORS headers only for origins listed in
WEB_ORIGIN; with none configured it sends none at all. There is no wildcard
mode.
A hardening checklist
Section titled “A hardening checklist”-
AUTH_JWT_SECRETis high-entropy and at least 32 characters -
CONFIG_ENCRYPTION_KEYis set and backed up -
WEB_ORIGINlists only origins you control - The database restricts network access to known addresses
- Secrets live in a platform secret store, not in files on disk
- You have decided, deliberately, whether the API is publicly reachable
- Database dumps are stored somewhere private
- Admin accounts are limited to people who need key access