Skip to content

Deployment Decisions

bitbiter-dev edited this page May 1, 2026 · 2 revisions

Deployment Decisions

Significant decisions that affect how you deploy or configure Anichron, recorded with context and rationale.


ADR-1 — Cross-origin deployment (CORS + SameSite)

Decision: Support same-origin (reverse proxy) and cross-origin (separate ports) deployments without code changes. Behaviour is driven by configuration.

Context: The UI and API run as separate containers, typically on different ports. Browsers treat different ports as different origins. SameSite=Strict cookies are silently stripped on cross-origin requests, breaking the refresh token flow.

Chosen approach:

  • CORS middleware with an AllowedOrigins whitelist from config (CORS__ALLOWED_ORIGINS env var)
  • Cookie set to SameSite=None; Secure when origins are configured; falls back to SameSite=Strict when AllowedOrigins is empty (same-origin / reverse proxy deployment)
  • Secure flag is omitted on localhost (browsers treat localhost as a secure context for SameSite=None)

Why this is secure: AllowedOrigins must never contain * when credentials (cookies) are involved — the browser rejects that combination. The whitelist is the security boundary; only listed origins can attach the cookie.

Configuration reference:

CORS__ALLOWED_ORIGINS=http://localhost:3000       ← local dev, cross-origin
CORS__ALLOWED_ORIGINS=https://photos.myhome.net  ← production without reverse proxy
# Leave unset when using a reverse proxy on the same origin

ADR-5 — GPU acceleration and multi-architecture support

Decision: The Worker detects available hardware acceleration at runtime and selects the best available encoder. A single Docker image is published for each supported architecture; there are no separate "gpu" and "no-gpu" image variants.

Encoder selection priority:

  1. Intel QuickSync (h264_qsv) — if /dev/dri is available and the driver responds
  2. NVIDIA NVENC (h264_nvenc) — if an NVIDIA GPU is present
  3. AMD AMF (h264_amf) — if an AMD GPU is present
  4. Software (libx264) — fallback, always available

Multi-architecture builds: CI publishes linux/amd64 and linux/arm64 manifests under the same image tag.

Intel media driver note: intel-media-va-driver is installed only on amd64 builds; the arm64 layer omits it. Runtime detection handles this gracefully.

/dev/dri passthrough in docker-compose.yml: Add the devices block for Intel QuickSync; omit it to fall back to software encode without error.


ADR-6 — Registration policy: admin-invite only

Decision: User registration requires an invite token issued by an admin. The POST /api/v1/auth/register endpoint rejects requests that do not carry a valid, unused invite token.

Context: A self-hosted app with open registration is a security risk — anyone who can reach the server can create an account. The first user (bootstrap admin) can register without an invite; all subsequent users require one.

Flow:

  1. Admin calls POST /api/v1/admin/invites → receives a single-use token
  2. Admin shares the token out-of-band (email, chat, etc.)
  3. User calls POST /api/v1/auth/register with { username, email, password, inviteToken }
  4. API validates and consumes the token; creates the user

Invite entity: Id, Token (hashed), CreatedByUserId, CreatedAt, ExpiresAt, RedeemedAt?, RedeemedByUserId?


ADR-9 — Observability

Decision: Structured JSON logging to stdout, a GET /api/v1/healthz health endpoint, and opt-in OpenTelemetry export.

Structured logging: Microsoft.Extensions.Logging with the JSON formatter enabled in production (ASPNETCORE_ENVIRONMENT=Production). Human-readable format in development.

Health endpoint (GET /api/v1/healthz): Checks DB connectivity and proxy storage writability. Returns 200 OK with a JSON body listing component statuses.

OpenTelemetry: Instrumented but no exporter configured by default. Set OTEL_EXPORTER_OTLP_ENDPOINT to enable. Covers HTTP request traces, DB query spans, Worker crawl spans, and per-file processing spans.

Privacy: File paths, filenames, and user-identifiable data are never logged or traced. Asset IDs (GUIDs) are acceptable.


ADR-11 — Exclusive NAS path ownership per user

Decision: Each NAS root path belongs to exactly one user. No two users share a UserStorageConfig pointing to the same path.

Rationale:

  • The global content_hash unique index is only valid when each file is processed by exactly one Worker. If two users shared a path, the second Worker would hit a unique constraint violation on content_hash.
  • Reconciliation (move detection, soft-delete) is unambiguous when path ownership is exclusive.
  • Simplifies the Worker startup: looking up a user by WORKER__USER and creating their UserStorageConfig is a single operation with no conflict risk.

Family libraries: Two users can both point their Workers at different sub-paths of a shared NAS (/nas/alice and /nas/bob), or one user's Worker can monitor the shared root. Interaction state is always private regardless.


ADR-12 — Single Worker processes all users; WORKER__USER optional for dedicated binding

Decision: By default a single Worker instance handles all users. WORKER__USER is an optional env var that pins a Worker to one specific user.

Default behaviour (no WORKER__USER set):

  1. On startup, load all UserStorageConfig records from the database
  2. Crawl each user's root in round-robin order so no single large library starves others
  3. One Worker container in docker-compose.yml — no duplication needed for multi-user setups

Optional dedicated binding (WORKER__USER set):

  1. Worker loads only the UserStorageConfig records belonging to the specified user
  2. Useful for giving a specific user a dedicated Worker (e.g. a power user with a very large library), or for isolated testing
  3. Multiple Workers can run in parallel — one default Worker plus one or more dedicated Workers — without processing the same files twice, because the content_hash skip-if-exists check is idempotent

Context: An earlier design required one Worker per user, bound by WORKER__USER. This was operationally cumbersome — every new user meant editing docker-compose.yml and restarting. The single-Worker default eliminates that friction. Horizontal scaling with coordinated multi-Worker processing is tracked separately in Discussions.