Skip to content

The backend (the brain)

The backend is the one piece you can’t skip. It serves metadata, runs accounts and the login wall, and orchestrates playback through small grant endpoints. It holds no local state of its own — everything lives in PostgreSQL — so it scales horizontally.

  • Python 3.10+ (the Docker image uses 3.14-slim) — or just Docker.
  • A PostgreSQL database — reachable, with a user that can create tables. The bundled Compose file ships one for you; production should use a managed/external instance. See The database.
  • A TMDB API key — the only mandatory secret.
Terminal window
git clone https://gitlab.ramon.moe/crimsonhaven-to/crimson-backend.git
cd crimson-backend
cp .env.example .env # then edit it (see below)
docker compose up -d # brings up PostgreSQL + the API
curl http://localhost:8000/health

The Compose stack runs the API as a non-root user with a HEALTHCHECK on /health, and waits for PostgreSQL to be healthy before starting. The database schema is created automatically on first boot (idempotent migrations) — you only need an empty database.

Terminal window
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # edit it
uvicorn api:app --host 0.0.0.0 --port 8000

Run the tests with pip install -r requirements-dev.txt && pytest -q.

Edit .env. The bare minimum to boot:

TMDB_API_KEY=your_tmdb_read_access_token
DATABASE_URL=postgresql://crimson:crimson@localhost:5432/crimson
# Let yourself sign up; without a code, registration is closed:
SIGNUP_INVITE_CODE=some-code
# A stable secret for signing (generate: openssl rand -hex 32):
PROXY_SECRET=...

The Backend environment page documents every variable in detail. The most important groups:

  • DatabaseDATABASE_URL (or the discrete POSTGRES_* parts) + pool sizing.
  • Login wall & accountsREQUIRE_LOGIN, SIGNUP_INVITE_CODE, ADMIN_EMAILS, the optional SMTP_* for email accounts.
  • Proxy signingPROXY_SECRET + CRIMSON_PROXY_BASE for the /sign grant.
  • Operator-owned sourcesJELLYFIN_* and the cache worker.

A quick tour (full reference in the API docs at /docs on a running instance):

GroupExamplesPurpose
Content/search, /trending, /info, /seasons, /catalogueMetadata + browsing.
Watch/watch/{tmdb}/{s}/{e}, /watch/movie/{tmdb}The progressive NDJSON stream of resolved sources.
Grants/scrape-meta, /sign, /resolveHand the client what it can’t derive itself.
Operator proxies/jellyfin_proxy, /local_proxy, /cache_proxy, /playerServe your own media.
Accounts/auth/*, /account/*Sign-in, favorites, watch progress.
Extras/recommendations, /supporters, /changelog, /subtitles, /skiptimesOptional features.
Chat/chat, /chat/status, /chat/conversationsLumi’s chatbot. Optional, asleep by default, granted per account.

/watch doesn’t return one JSON body — it streams NDJSON (one JSON object per line) so each resolved source reaches the player the instant it’s ready:

{"type":"meta","tmdb_id":1234,"season_number":1,"episode_number":1,"title":""}
{"type":"stream","source":"Jellyfin","streamType":"hls","url":"https://…/jellyfin_proxy/…"}
{"type":"done","count":1}

The client’s in-browser engine emits the same line shape for the sources it resolves, and the two are merged into one list. This is the seam that lets scraping move to the browser without changing the player.

The backend can serve three kinds of media you control (not third-party scraping):

  • Local — browser-playable files in directories / NAS mounts you register in the admin dashboard. Served via /local_proxy with seeking support.
  • Cache — episodes the server already remuxed onto your NAS (the optional cache worker). Served via /cache_proxy.
  • Jellyfin — your own Jellyfin server, configured by JELLYFIN_* env. The backend injects the access token server-side so it never reaches the browser.

See Operator-owned sources to enable each.

The backend is stateless, so you can run many replicas behind a load balancer. The two rules when you do:

  1. Set RUN_DB_SYNC=true on exactly one replica (the periodic mapping rebuild must run once).
  2. Set the same PROXY_SECRET on every replica so signed links verify anywhere.

The Swarm deployment page covers high-availability PostgreSQL, connection pooling and backups.