-
Notifications
You must be signed in to change notification settings - Fork 0
Deployment Decisions
Significant decisions that affect how you deploy or configure Anichron, recorded with context and rationale.
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
AllowedOriginswhitelist from config (CORS__ALLOWED_ORIGINSenv var) - Cookie set to
SameSite=None; Securewhen origins are configured; falls back toSameSite=StrictwhenAllowedOriginsis empty (same-origin / reverse proxy deployment) -
Secureflag is omitted onlocalhost(browsers treat localhost as a secure context forSameSite=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
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:
- Intel QuickSync (
h264_qsv) — if/dev/driis available and the driver responds - NVIDIA NVENC (
h264_nvenc) — if an NVIDIA GPU is present - AMD AMF (
h264_amf) — if an AMD GPU is present - 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.
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:
- Admin calls
POST /api/v1/admin/invites→ receives a single-use token - Admin shares the token out-of-band (email, chat, etc.)
- User calls
POST /api/v1/auth/registerwith{ username, email, password, inviteToken } - API validates and consumes the token; creates the user
Invite entity: Id, Token (hashed), CreatedByUserId, CreatedAt, ExpiresAt, RedeemedAt?, RedeemedByUserId?
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.
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_hashunique 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 oncontent_hash. - Reconciliation (move detection, soft-delete) is unambiguous when path ownership is exclusive.
- Simplifies the Worker startup: looking up a user by
WORKER__USERand creating theirUserStorageConfigis 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.
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):
- On startup, load all
UserStorageConfigrecords from the database - Crawl each user's root in round-robin order so no single large library starves others
- One Worker container in
docker-compose.yml— no duplication needed for multi-user setups
Optional dedicated binding (WORKER__USER set):
- Worker loads only the
UserStorageConfigrecords belonging to the specified user - Useful for giving a specific user a dedicated Worker (e.g. a power user with a very large library), or for isolated testing
- Multiple Workers can run in parallel — one default Worker plus one or more dedicated Workers — without processing the same files twice, because the
content_hashskip-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.
Anichron
Architecture
- Solution Overview
- Database Architecture
- Architecture Diagrams
- Deployment Decisions
- Engineering Decisions
- Versioning and Releases
Tracking