The client (the frontend)
The client is the website your visitors see: search, the browse hubs, the player, accounts. It is a React + Vite single-page app, served in production by an Nginx image with security headers.
Sources are bundled at build time
Section titled “Sources are bundled at build time”The client bundles your private sources engine as a git submodule at
vendor/crimson-sources.
The companion extension used to be vendored here too. It now ships on the
Chrome Web Store, and the
/extension page only links to the listing.
The sources are not required to build. If vendor/crimson-sources is absent (no
access, or no sources repo yet), vite.config.js swaps in a built-in no-op
(src/sourcesStub.js) and the build succeeds with no client-side sources. Playback
then falls back to the backend.
That gives two valid setups:
- Sources present (the full experience): point the submodule at your own private repo. See Adding your own sources.
- No sources: a metadata and accounts site that builds out of the box.
The API base URL is baked in at build time
Section titled “The API base URL is baked in at build time”The client talks to one backend, and its address is compiled into the bundle via the
build argument VITE_API_BASE_URL. Set it when you build, not at runtime:
VITE_API_BASE_URL=https://backend.example.com docker compose up --build -dChanging backends means rebuilding. A static SPA has no server to read an env var at request time.
Deployment-specific text (optional build args)
Section titled “Deployment-specific text (optional build args)”Two on-page strings are build arguments, so you can brand a fork without touching the source. Both have defaults, so a plain build works unchanged:
| Build arg | Controls | Default |
|---|---|---|
VITE_HOSTED_IN |
Where user data lives, shown in the About page’s “Queen’s Decree”, the footer pills and the welcome tour. | Switzerland |
VITE_DMCA_MAIL |
The takedown / DMCA contact address on the Disclaimer page. | service@crimsonhaven.to |
They are baked in exactly like VITE_API_BASE_URL:
docker build \ --build-arg VITE_API_BASE_URL=https://backend.example.com \ --build-arg VITE_HOSTED_IN="🇩🇪 Germany" \ --build-arg VITE_DMCA_MAIL=abuse@example.com \ -t crimson-client:1.0 .VITE_HOSTED_IN accepts a flag emoji (e.g. 🇨🇭 Switzerland). In CI they come from
the GitLab CI/CD variables HOSTED_IN and DMCA_MAIL (see
The CI/CD pipeline). Leave them unset to keep the defaults.
Social link previews (Open Graph) are per-environment
Section titled “Social link previews (Open Graph) are per-environment”The preview card Discord, Twitter or Slack show for your link comes from the Open
Graph / Twitter <meta> tags in index.html, including og:image. Those need
absolute URLs, and the scrapers that read them don’t run JavaScript, so the origin
must be in the served HTML. It cannot be fixed up at runtime with window.location.
The origin is baked in via VITE_SITE_URL, which replaces every __SITE_URL__ token
in index.html (see vite.config.js):
docker build \ --build-arg VITE_API_BASE_URL=https://backend.example.com \ --build-arg VITE_SITE_URL=https://example.com \ -t crimson-client:1.0 .Set it to the exact origin this build is served from, without a trailing slash:
https://dev.example.com for the dev image, https://example.com for prod. Unset, it
falls back to the production origin. The reference CI sets it per channel alongside
VITE_API_BASE_URL.
The card image lives in public/ (e.g. public/crimson_embed.png) and is served at
the site root. Replace that file to rebrand the preview.
Building and serving
Section titled “Building and serving”Local development
Section titled “Local development”git clone https://gitlab.ramon.moe/crimsonhaven-to/crimson-client.gitcd crimson-clientgit submodule update --init --recursive # optional: without access, the stub is usednpm installnpm run dev # Vite dev serverProduction (Docker)
Section titled “Production (Docker)”The Dockerfile is multi-stage: Vite compiles the static bundle, then Nginx serves it
on port 80 with gzip, security headers, immutable hashed assets and a /healthz
probe.
docker build \ --build-arg VITE_API_BASE_URL=https://backend.example.com \ -t crimson-client:1.0 .docker run -p 8080:80 crimson-client:1.0curl http://localhost:8080/healthz # -> okOr with the bundled Compose file, which publishes it on 8080:
VITE_API_BASE_URL=https://backend.example.com docker compose up --build -dWhere things live
Section titled “Where things live”| File | Role |
|---|---|
src/hooks.js |
API calls, stream handling, cryptographic session state. |
src/clientSources.js |
The bridge to your bundled sources engine and the /scrape-meta, /sign, /resolve grants. |
src/CrimsonPlayer.jsx |
The custom HLS/MP4 player. |
src/Account.jsx |
Mnemonic and email account flows. |
src/AccountSecurity.jsx |
Sessions, recent activity, data export and self-deletion, on the Account page. |
src/AiringCalendar.jsx, src/FollowButton.jsx |
The /calendar page and the follow bell beside the watchlist control. |
src/CrimsonWrapped.jsx |
The /wrapped year in review, from the account dropdown. |
src/AnimeHub.jsx, src/ShowsHub.jsx, src/MoviesHub.jsx |
The per-type browse hubs (/catalogue redirects to /anime). |
src/AnimeOverview.jsx, src/ShowOverview.jsx, src/MovieOverview.jsx |
Per-title pages. |
src/DownloadExtension.jsx |
The /extension page, linking to the companion’s Chrome Web Store listing. |
src/offline/ |
Offline downloads of movies and episodes: the Save offline dialog, the queue, /downloads. See Offline downloads. |
security-headers.conf, nginx.conf |
The Nginx serving config. |
Client-side resolution
Section titled “Client-side resolution”The client does not only consume the backend’s /watch stream. It also runs your
sources engine in the browser and merges the results, with a locally resolved
stream replacing a backend duplicate of the same source. This stays off unless your
engine reports it can run something, so a stub build or a visitor without the
companion falls back to the backend. The mechanics are in the
New System.
A required CSP note
Section titled “A required CSP note”For the in-browser player to load streams from rotating CDNs, the Content-Security-Policy
in security-headers.conf sets connect-src 'self' https:. The strict script-src
(the real XSS protection) is untouched: this only widens where the page may connect,
which client-side playback needs.
Installable web app (PWA)
Section titled “Installable web app (PWA)”The client ships a web manifest and a service worker (public/sw.js), so browsers
offer “Install app” / “Add to Home Screen”.
- The worker only handles same-origin GET requests. The backend API, auth, progress calls and the NDJSON stream live on another origin and go straight to the network.
- At build time the
swPrecacheplugin invite.config.jswrites the build’s JS, CSS and font chunks into the worker, together with a build id. Every deploy installs a new worker that precaches all chunks, so lazy pages load offline even if they were never opened online. - Navigations are network-first with the cached shell as offline fallback; other same-origin assets are stale-while-revalidate.
- Music and videos a member downloads for offline use live in separate
crimson-music-*andcrimson-video-*caches, which a worker update never deletes. See Offline downloads.
