Scanta
in-progressA C#/.NET photo-import pipeline with desktop capture, service auth, shared contracts, and idempotent imports into Immich.
Overview
Scanta digitizes a family photo archive into a self-hosted Immich instance. The hard part isn’t the scanning; it’s the path: getting scans from a Windows desktop into a homelab that I deliberately keep off the public internet, with the Immich API key never leaving that homelab.
The project is really about service boundaries. Scanta moves each batch through small C#/.NET pieces that prove who they are to each other, share one contract, and can recover when an import stops halfway through:
[Windows desktop] --upload--> [VPS scandrop] --forward--> [homelab importworker] --> [Immich]
How it’s built
- Four C#/.NET pieces. A desktop scanner (Avalonia + NAPS2) for capture, date hints,
album selection, and crop/deskew; a public-facing ScanDrop intake (ASP.NET Core) that
authenticates clients, validates, and stages uploads; an ImportWorker (.NET Worker +
EF Core) that runs next to Immich and imports staged batches; and
Scanta.Contracts, a single shared assembly of DTOs and enums so “compiles” means “will integrate.” - Auth at the seams. Desktop clients hold opaque, scoped, peppered-hash tokens; the admin UI is gated by Authentik OIDC; the ScanDrop -> ImportWorker hop requires a shared service-token bearer on every endpoint. The homelab is reachable only over Tailscale, so the network controls who can reach the port and the token controls who gets in.
- Durable, forward-only imports. An EF Core/SQLite ledger records every asset and album action, so a crashed import resumes idempotently instead of rolling back or duplicating photos.
- Tested against the contract. xUnit unit tests plus
WebApplicationFactoryHTTP and contract tests that POST the canonical fixtures embedded inScanta.Contractsverbatim. Deployment artifacts and runbooks exist too: ScanDrop is Dokku-oriented, while the ImportWorker is set up for Docker Compose on the homelab side.
Status
In progress, building milestone by milestone. The desktop app, shared contract, ScanDrop intake, token/OIDC/service-token auth model, staging and queueing pipeline, and EF Core ledger with idempotent retry all work today. The ImportWorker now has a real handwritten Immich client and live-test path, while the normal automated suite still runs against fakes. Crop/deskew exists as a best-effort/manual desktop pipeline; remaining work is production deployment verification and hardening the scanner-quality edge cases. The ImportWorker node also doubles as the archive’s backup target.