AUTH_KEY, optional manually generated API keys and scoped OAuth connection tokens. Manually generated keys carry explicit permission ceilings for the single health ledger; OAuth tokens authorize only their requested MCP scopes. The primary key administers all authentication records. The signed-in UI can manage generated keys and MCP connections after a separate root-credential verification. Key-management operations are REST-only and do not add MCP tools.
An MCP OAuth client signs in with the same root credentials and creates a scoped 30-day connection token through authorization-code exchange. There are no additional accounts, JWTs, refresh tokens or health-ledger owners.
Configuration and generation
ConfigureROOT_EMAIL and ROOT_PASSWORD through your deployment’s secret manager. Use at least 15 non-padding characters for the password, at most 256 UTF-8 bytes, and a different value from AUTH_KEY. Both values must be supplied together. Leave both empty to disable generation; existing primary and generated Bearer keys still work. Changing root credentials requires an API restart and affects new issuance only.
Open https://vitalog.example.com/api-keys on your installation, or use Account Settings → API Keys. Choose a Name, Permissions and Expires after, then enter the root email and password. Permissions are Read-only, Edit or Administrative permissions. Expiry choices are 30 days, 90 days, 1 year or Never. The choices start blank and are required. The form sends a JSON POST and returns one token. Copy it immediately; it cannot be retrieved later. The password is cleared after submission, and neither credentials nor tokens are stored in browser storage. The page has no third-party assets, cannot be framed and uses a restrictive CSP and Cache-Control: no-store.
The page runs in the separate Next.js app, using React 19 and HeroUI v3 with Tailwind CSS v4, Inter and a red theme. It posts to https://vitalog-api.example.com/auth/api-keys. The API accepts browser requests only from its configured UI_BASE_URL on this endpoint, with JSON-only preflight. The UI uses a fresh script nonce for each uncached response, allows network calls only to itself and the configured API, blocks native form submissions and framing, and serves its font/logo locally. HeroUI requires inline component styles; scripts require a nonce. The UI image and runtime environment contain no API credentials. Invalid login shows the HeroUI “Invalid credentials” toast and clears the password.
Direct generation uses POST /auth/api-keys with Content-Type: application/json. This route authenticates using the JSON credentials and requires no Bearer header:
access is read or edit; includeAdmin is a boolean. Administrative permissions require access: "edit". expiresAt is a future UTC timestamp or explicit null for Never. Omitting any setting fails validation. An optional UUID Idempotency-Key header makes creation safe to retry with the same body. A replay returns the original metadata with api_key: null; the complete token appears only in the original response. Reusing the identifier with changed settings returns 409. The browser retains the same timestamp and identifier through uncertain retries. Requests are limited to 4 KiB. Invalid email/password pairs return the same 401 response. Issuance permits five attempts per socket/trusted client address per minute, thirty total per process per minute and at most two concurrent password verifications. Limits include successful issuance. Untrusted forwarded IP headers do not change the bucket. A 429 includes Retry-After: 60; limits reset after process restart.
Email matching is case insensitive and ignores surrounding whitespace; password matching is exact. The API derives a salted scrypt verifier at startup (N=32768, r=8, p=3) and performs asynchronous verification. Root credentials are not stored in PostgreSQL. The token contains 32 random bytes under the vlk_ prefix and is stored only as a SHA-256 hash. Its chosen expiry is stored as an absolute UTC timestamp; Never is stored as null. Browser sessions still expire after 720 hours, management sessions after 30 minutes, and OAuth connection tokens after at most 720 hours.
A successful 201 response contains api_key, id, name, access, includeAdmin, token_hint, created_at, expires_at, revoked_at and status. Timestamps are UTC ISO strings. The api_key field occurs only in this creation response. Metadata status is active, expired or revoked; revocation takes precedence over expiration. Migration 0015_personal_api_key_policy adds the manual-key name and explicit permissions. Existing manual keys receive a stable name and Edit access without administration, preserving their health read/write ceiling. Token hashes, IDs, expiry, revocation, ledger data and OAuth/session bindings are preserved. Only manual keys permit null expiry.
The generation endpoint accepts browser submissions from the exact configured UI origin, or loopback HTTP during development. Other origins and form-encoded requests are rejected. If UI_BASE_URL is absent, direct same-origin API clients retain their previous behavior. Do not put root credentials in URLs or HTTP Basic authentication. Keep reverse-proxy request/response bodies and Authorization headers out of logging and tracing.
Use and management
In the signed-in UI, open Settings → API Keys at/settings/api-keys. The existing dashboard session lists keys immediately. Verify the root email and password to unlock a 30-minute credential-management session when creating or revoking keys. Create a key with the required name, permissions and expiry, revoke one key or revoke all manually generated keys. Settings → MCP Connections at /settings/mcp-connections separately lists OAuth clients, approved permissions and expiry; revoke a connection there to disconnect that client. Revoked rows are filtered before pagination. The browser’s read-only health session stays signed in when keys are revoked here. Settings → MCP Guide provides OAuth configurations for Codex, Claude Code, Cursor, VS Code and other MCP clients.
The UI stores the opaque vlm_ management session in a separate host-only HttpOnly SameSite=Lax cookie. It carries only keys:manage for urn:vitalog:key-management; it cannot read or write health records, access MCP or use primary-key administration. The server proxies the narrow /auth/key-management/* API, checking the dashboard session and exact Origin on mutations. Generated keys and ordinary browser sessions cannot unlock creation or revocation. A browser session can list metadata through GET /auth/key-management/api-keys. Logout revokes both browser sessions. Migration 0010_brainy_fat_cobra fixes management-key expiry at 30 minutes and permits the new scope only on this dedicated resource; OAuth health scopes remain unchanged.
POST /auth/key-management/session accepts only the strict JSON email and password body and shares the sign-in rate limit. GET checks the authenticated management session and DELETE revokes it. The session authorizes GET, POST and DELETE /auth/key-management/api-keys, plus DELETE /auth/key-management/api-keys/{id}. Creation requires a JSON body containing name, access, includeAdmin and expiresAt, without root credentials; it accepts the same optional UUID retry header. Optional kind=api-key or kind=mcp filters management listing before pagination and scopes collection revocation. Revoking manual keys leaves pending OAuth codes and MCP connections intact. Omitting kind retains combined administration. Management listing and revocation exclude browser and management sessions. Migration 0014_mcp_connection_metadata records OAuth client IDs, names and approved scopes without changing existing token hashes or expiry. The primary endpoints below retain their full scope, including those session records.
Supply Authorization: Bearer <generated key> privately on every REST or MCP request, including initialization and discovery. Read-only keys read health data through REST and MCP; Edit keys can also write it. Administrative permissions include health read/write and inspection of /readyz and /openapi.json. Account settings, credential creation/reveal and key management remain protected by browser/root authority. Every request checks the stored ceiling. MCP discovery exposes only permitted tools and tool calls enforce the same ceiling, including when no OAuth issuer is configured. OAuth scope grants are intersected with a manual parent key ceiling where applicable. Expired, revoked, unknown and malformed keys return 401. Generated keys attempting administration return 403. A revoked key is rejected on the next request, even from an already connected MCP client; previously authenticated requests may complete.
Use the primary environment AUTH_KEY for these administration APIs:
Listing defaults to 50 rows and accepts a limit from 1 to 100 and offset from 0 to 1000000. It orders newest creation first, breaking ties by ID. Duplicate or unknown query parameters are rejected. Revocation accepts no request body or query. A missing key ID returns 404; repeating revocation preserves the original revocation timestamp. Revoke-all includes expired keys, is safe to repeat, and never changes
AUTH_KEY. Keys created after revoke-all can be used normally. Metadata is retained for expired and revoked keys.
All state survives process restarts and primary-key rotation. No authentication cache delays revocation. Root credential changes do not implicitly revoke keys. Database backups include token hashes and revocation metadata; restoring an older backup can re-enable a key revoked after that backup. Use the primary key to revoke restored generated keys before exposing a restored instance.
These are bearer API keys. MCP clients connect through the OAuth flow by signing in with the configured root email and password. Code exchange creates the 30-day MCP token and an API-key management record with a vlo_… hint. It appears on the MCP Connections page; revoking it disconnects that client on the next request. Unfiltered administration or MCP-scoped revoke-all also cancels pending authorization codes. Authentication attempts and password-verification concurrency are shared across manual key creation and OAuth sign-in.
Verification
Runnpm run test:web for the sidebar, Settings and credential-management boundaries, npm run test:auth for database/API verification and npm run test:container for the independent production UI. The auth check creates and removes a disposable PostgreSQL database. The auth check verifies the absence of UI assets in the API, a populated-database forward migration, one-time issuance and hash-only storage, both transports, administration boundaries, exact expiry, revocation, restart/rotation durability, login limits, origin protection, configuration fallback and log privacy. Reports are written to ignored .test-artifacts/auth.json and uploaded by CI. No production database is used.