Migrations abort: DB secret preflight failed
Symptom: the one-shot migrations service exits immediately with:compose.selfhost.yml) runs a fail-closed preflight before migrating or provisioning roles. It refuses to continue when POSTGRES_PASSWORD or APP_USER_PASSWORD is:
- empty,
- still a
change-me-*placeholder from.env.selfhost.example, - a known public dev default shipped in the repo (
app-user-dev,3ngram-dev), or - shorter than 12 characters.
:? guards only catch missing variables, not unchanged placeholders — the preflight closes that gap.
Fix: set strong, URL-safe values for both secrets in .env.selfhost, then re-run the init profile:
The local development stack (
docker-compose.yml) keeps its dev defaults and never runs this preflight. The abort only applies to the self-host compose file.Auth error after changing the runtime password
Symptom: the server or the host-run seed fails with a Postgres password-authentication error forapp_user right after you changed APP_USER_PASSWORD.
Cause: two consumers read the runtime password from different places. Compose derives the server’s connection string from APP_USER_PASSWORD, while anything running on the host (the seed, a manual psql) reads DATABASE_URL literally. Updating one without the other leaves the two out of sync. A related variant: a shell you sourced before editing the env file still carries the old password in its exported DATABASE_URL.
Fix: update both places, then re-source your shell before running host-side commands:
MCP clients cannot authenticate: OAuth discovery misconfigured
Symptom: one of the following:- the server is unhealthy at boot under
NODE_ENV=production, - an MCP client fails during the OAuth flow (discovery or token-audience errors),
/mcpreturns401even though your API key works on/api/v1.
BASE_URL. In production the config fails closed at boot: BASE_URL and OAUTH_JWKS are both required, and a BASE_URL that is not an absolute http(s) URL is treated as not configured. If BASE_URL does not exactly match the public origin your clients connect to (for example, the server sits behind a TLS proxy but BASE_URL still points at the internal address), discovery metadata and token audiences will not line up and the flow fails.
Fix:
-
Set
BASE_URLto the public HTTPS origin clients actually use (the reverse-proxy origin, not the container address). -
Set
OAUTH_JWKSto a JSON array of RS256 signing keys. Bootstrap or rotate with the shipped script — the first key in the array is the current signing key: -
Verify discovery responds at
BASE_URL:/.well-known/oauth-protected-resource/mcpand/.well-known/jwks.jsonshould both return200.
401 from /mcp with an X-API-Key header is by design, not a misconfiguration: API keys work on the REST surface (/api/v1) only, while /mcp accepts OAuth Bearer tokens.
Rate limiting degraded: “store unavailable”
Symptom: repeated warnings in the server logs:429s to alert on. A limiter explicitly configured to fail closed returns 503 service_unavailable on its routes instead.
Fix: restore Redis connectivity (REDIS_URL), then confirm the warnings stop. Treat repeated occurrences as a degraded-protection incident, not noise — the warning and its failure counter metric are the only signals that throttling is off. See Operate for the limit dimensions.