-
Notifications
You must be signed in to change notification settings - Fork 0
Engineering Decisions
Significant decisions that affect how Anichron is built and extended, recorded with context and rationale.
Decision: Both email addresses and usernames are stored fully lowercased (ToLowerInvariant()) in the database. Normalization happens before any uniqueness check and before persisting.
Rationale: PostgreSQL B-Tree indexes are case-sensitive by default. Storing original-case values while comparing lowercased values during validation creates a silent gap: Alice and alice would pass the application-level duplicate check (both normalize to alice) but the SaveChangesAsync would succeed for the first and hit a unique-index violation for the second only if the first was already committed. More critically, without normalizing the stored value, login via alice@example.com would fail for a user registered as Alice@example.com because the lookup query compares lowercased input against the stored value.
RFC 5321 technically permits case-sensitive local parts in email addresses, but no real provider distinguishes them in practice. For a personal media vault, case-preserving identity adds friction without benefit.
Scope: Usernames also have no case-preserving brand value in this family-application context.
Decision: Proxy files are stored under /data/proxies/{first2}/{rest}/{type} where {first2} and {rest} are derived from the asset's UUID (hex, lowercase), and {type} is the proxy type slug.
Example:
/data/proxies/f3/a1b2c4d5e6f7.../thumbnail.jpg
/data/proxies/f3/a1b2c4d5e6f7.../preview.jpg
/data/proxies/f3/a1b2c4d5e6f7.../video_720p.mp4
/data/proxies/f3/a1b2c4d5e6f7.../blurhash.txt
Rationale: All proxy types for one asset share the same directory — atomic cleanup when an asset is permanently deleted. Two-level sharding (like git objects) prevents any single directory from growing too large on libraries with hundreds of thousands of assets.
Proxy type slugs: thumbnail, preview, video_720p, blurhash
ProxyFile.FilePath stores the path relative to /data/proxies/ (e.g., f3/a1b2c4d5.../thumbnail.jpg). The serving endpoint prepends the base path.
Decision: AssetInteraction state affects which assets appear in flashback queries:
| State | Effect |
|---|---|
hidden = true |
Always excluded from all flashback queries. Applied as a global EF Core filter alongside the soft-delete filter. |
starred = true |
Always included and sorted first within its year group in the On This Day view. |
liked = true |
Asset receives a higher display weight — shown more frequently when a date has many candidates across years. AssetInteraction.DisplayWeight is set to 2.0 (default 1.0). |
Implementation note: hidden exclusion is enforced at the EF Core query layer. starred and liked affect sort order and candidate selection in Epic 5 query logic; they do not change the data model structure.
Decision: The Worker runs a crawl on a configurable interval. Default: every 4 hours.
Configuration key: Worker:CrawlIntervalHours (integer). Override via env var WORKER__CRAWL_INTERVAL_HOURS.
Behaviour: On startup the Worker performs an immediate crawl, then sleeps for the configured interval, then repeats indefinitely.
Rationale: A configurable interval inside the process is the simplest model for self-hosted deployments. Continuous filesystem watching is unreliable over NFS/SMB network mounts; polling is more robust.
Decision: All API endpoints are prefixed with /api/v1/. Example: POST /api/v1/auth/login.
Rationale: Prevents a breaking URL change if a v2 is ever needed. Separates API routes from any future UI static-file serving on the same origin.
Decision: A bounded-concurrency Channel<T> pipeline. The crawler produces file paths; N consumer tasks process them concurrently. Concurrency is configurable via Worker:MaxConcurrentFiles (default 4).
Context: A 200 GB library could contain tens of thousands of files. Sequential processing is too slow for first-ingest but a full job-queue framework (e.g. Hangfire) is premature. The key constraint is that processing must survive Worker restarts — handled by the content_hash skip-if-exists check; the pipeline is fully idempotent.
Upgrade path: Replacing the channel with a persistent job queue later requires only changing the producer/consumer wiring, not the processing logic itself.
Decision: Anichron is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).
Rationale: Ensures that anyone who runs a modified version — including as a network service — must publish their changes under the same terms. Protects against commercial SaaS forks while keeping the project fully open for self-hosters and contributors.
Implication: A LICENSE file must exist at the repository root. Individual source files do not require licence headers.
Decision: API and Worker images are published as multi-arch manifests covering linux/amd64 and linux/arm64 under a single tag.
Build stage runs on the CI host, not under emulation. FROM --platform=$BUILDPLATFORM on the SDK stage forces compilation to run natively on the amd64 CI runner. dotnet publish produces arch-neutral IL bytecode which the JIT compiler in the final container translates at startup — the same output is correct for both architectures, so native compilation is safe and avoids 10–20× QEMU overhead.
Runtime stage uses the target arch. No --platform override on the base image stage; BuildKit pulls the amd64 or arm64 runtime variant as appropriate. The IL output from the build stage is copied in unchanged.
QEMU enables foreign-arch RUN commands. docker/setup-qemu-action@v3 registers QEMU with the Linux kernel so arm64 layer commands (e.g. apt-get install) run transparently on the amd64 CI runner.
TARGETARCH for conditional package installs. BuildKit injects this variable automatically. Used in the Worker to skip intel-media-va-driver-non-free on arm64 — the package does not exist in arm64 apt repos.
Why not separate GPU/CPU image variants? A single image with runtime hardware detection is simpler to operate. See ADR-5.
Anichron
Architecture
- Solution Overview
- Database Architecture
- Architecture Diagrams
- Deployment Decisions
- Engineering Decisions
- Versioning and Releases
Tracking