← All projects

Scanta

in-progress

A C#/.NET photo-import pipeline with desktop capture, service auth, shared contracts, and idempotent imports into Immich.

C#/.NETMicroservicesAuthenticationASP.NET CoreSelf-hosted

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 WebApplicationFactory HTTP and contract tests that POST the canonical fixtures embedded in Scanta.Contracts verbatim. 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.