Skip to content

Troubleshooting

When the castle misbehaves, start here. Each entry is symptom → cause → fix.

The client built, but has no sources / playback

Section titled “The client built, but has no sources / playback”

Cause: vendor/crimson-sources wasn’t present at build time, so the safeguard bundled the no-op stub (the build log says “bundling the no-op stub”). That’s expected for a sources-free site. Fix (if you wanted sources): set the CRIMSON_SOURCES_REPO secret to your private sources repo and SUBMODULES_TOKEN to a PAT with read access, then rebuild — see Adding your own sources. The build never fails over missing sources; it just falls back.

The backend won’t start / can’t reach the database

Section titled “The backend won’t start / can’t reach the database”

Cause: wrong DATABASE_URL, or the database isn’t up yet. Fix: check docker compose logs backend; confirm PostgreSQL is healthy; verify the credentials. The backend waits up to DB_CONNECT_TIMEOUT seconds at boot.

docker compose up ignores my new .env values

Section titled “docker compose up ignores my new .env values”

Cause: Compose/Swarm only inject variables listed in the service’s environment: block; .env is used for ${...} substitution, not auto-injection. Fix: ensure the variable is referenced in the compose file, then recreate the container (docker compose up -d).

Cause: the members-only login wall (REQUIRE_LOGIN=true) with no valid session. Fix: that’s expected — log in. To open the API entirely (e.g. a demo), set REQUIRE_LOGIN=false.

Cause: SIGNUP_INVITE_CODE is empty (signups closed) or the user didn’t enter a valid code. Fix: set SIGNUP_INVITE_CODE, recreate the container, and have users enter it at signup.

After login I’m immediately logged out (iOS / Safari)

Section titled “After login I’m immediately logged out (iOS / Safari)”

Cause: WebKit drops the Authorization header when following a redirect, so a redirected request hits the login wall unauthenticated. Fix: this is handled by the backend serving directly instead of redirecting; if you’ve customised routing, avoid 301-redirecting authenticated API calls.

Streams are blocked as “mixed content”

Section titled “Streams are blocked as “mixed content””

Cause: the backend emitted http:// URLs because it didn’t see it was behind HTTPS. Fix: make your reverse proxy set X-Forwarded-Proto: https (and X-Forwarded-Host), and set FORWARDED_ALLOW_IPS=* so uvicorn trusts them.

”This content is blocked” / CSP errors in the console

Section titled “”This content is blocked” / CSP errors in the console”

Cause: the in-browser player must connect to rotating hoster CDNs, which the page’s connect-src CSP must allow. Fix: the client ships connect-src 'self' https: in security-headers.conf for exactly this; if you’ve tightened it, you’ll block playback. (script-src stays strict — only connect-src is widened.)

CORS errors loading /cache_proxy or subtitles

Section titled “CORS errors loading /cache_proxy or subtitles”

Cause: a cross-subdomain request became CORS-enforced (e.g. a <video crossorigin> when subtitle tracks are present) and the response lacked the header. Fix: ensure ALLOWED_ORIGINS includes your client’s exact HTTPS origin and the container actually sees that value; check the failing request’s status (a 404 means the cached file moved / its target was disabled, not a CORS bug).

The proxy path does nothing / /sign returns 503

Section titled “The proxy path does nothing / /sign returns 503”

Cause: CRIMSON_PROXY_BASE isn’t set on the backend (or wasn’t injected into the container), so the E2 path is disabled. Fix: set CRIMSON_PROXY_BASE (and make sure it’s in the compose environment: block), deploy the proxy with a matching NITRO_PROXY_SECRET == PROXY_SECRET, and recreate the backend.

Cause: one edge host (e.g. Netlify) is unhealthy while another (Cloudflare) works, and requests were landing randomly. Fix: the backend health-checks edges and routes only to healthy ones — make sure both hosts in CRIMSON_PROXY_BASE are actually deployed and reachable, or list only the working one.

Cause: it isn’t loaded/enabled, or its content script isn’t injecting on your hostname. Fix: confirm it’s enabled at chrome://extensions and shows the current version; confirm your site’s hostname matches the extension’s allowed origins (it targets Crimson origins + localhost). In the site console, check window.CrimsonExtension?.available.

Extension is detected but streams 403 after a few seconds

Section titled “Extension is detected but streams 403 after a few seconds”

Cause: the header-injection rules were torn down mid-playback. Fix: this is handled by keeping media rules alive through playback (cleared on the next episode); if you’ve modified the client engine lifecycle, don’t dispose the engine on resolve completion.

Some hosts fail with “intercepted by a content blocker”

Section titled “Some hosts fail with “intercepted by a content blocker””

Cause: a co-installed blocker (AdGuard/uBlock) is substituting a stub for the media. Fix: the blocker fetches happen in the extension’s service worker (no tab context), so a per-site allowlist may not help — pause the blocker or disable the specific rule.

No summon button appears, even though she’s switched on

Section titled “No summon button appears, even though she’s switched on”

Cause: almost always the missing per-account grant. Access is deny-by-default and the drawer renders nothing at all rather than showing a disabled button. Fix: grant that member on Admin › Users (the bot icon next to the admin toggle), then reload. If it’s still absent, check GET /chat/status: granted:false is the grant, enabled:false is the master switch on Admin › Lumi, and configured:false is a missing API key. Note she’s also hidden by design on the watch pages.

Every message answers 503 “no oracle configured”

Section titled “Every message answers 503 “no oracle configured””

Cause: the container can’t see a provider key. Compose and Swarm only inject variables explicitly listed in a service’s environment: block, so a key that’s in your .env alone never reaches the app. Fix: add ANTHROPIC_API_KEY / GEMINI_API_KEY to that block and recreate the container. Admin › Lumi shows key present once it lands.

The key is “present” but every call fails with 400 API_KEY_INVALID

Section titled “The key is “present” but every call fails with 400 API_KEY_INVALID”

Cause: a missing $ in the compose file. {GEMINI_API_KEY:-} is passed through as that literal string, which reads as present to the feature gate and then fails upstream. Fix: write ${GEMINI_API_KEY:-}, and recreate the container.

She refuses with “you have exhausted this month’s audience”

Section titled “She refuses with “you have exhausted this month’s audience””

Cause: that member hit their monthly token budget, or an admin froze them with an explicit budget of 0. Fix: raise the haven-wide budget on Admin › Lumi, or set a per-member override via PATCH /admin/users/{id}. Budgets reset at the start of each calendar month. See Lumi, the chatbot.

Saving the settings is rejected, or Claude isn’t selectable

Section titled “Saving the settings is rejected, or Claude isn’t selectable”

Cause: switching her on with no key for the selected provider is refused up front (better than a 503 for whoever opens the drawer first), and pairing a model with the wrong vendor is refused too. If the tab warns that the anthropic package is missing, this build was stripped of the optional SDK and only Gemini can answer. Fix: set the matching key, pick a model from that provider’s list, and rebuild the image if you want the Claude path back.