Storage abstracted behind a provider interface; Azure Blob added
Two commits refactor `backend/src/lib/storage.ts`. The first (`45310480`) extracts a `StorageProvider` interface and wraps R2/S3 in an `R2Provider` class with no behavior change. The second (`c3daf8fa`) adds an `AzureBlobProvider` against that interface, supporting both connection-string auth (local/Azurite) and `DefaultAzureCredential` (Managed Identity in Container Apps).
The interface itself is four methods: upload, download, remove, and signedUrl. The signedUrl return type is string | null - providers that can't issue direct browser URLs return null, and callers fall back to the backend download proxy at GET /download/:token. That's how the Azure provider works: signedUrl() always returns null, and downloads route through the proxy instead of generating presigned blob URLs.
AzureBlobProvider auth resolves in priority order: AZURE_STORAGE_CONNECTION_STRING first (Azurite uses UseDevelopmentStorage=true), then AZURE_STORAGE_ACCOUNT_NAME with DefaultAzureCredential. The container name defaults to documents and can be overridden with AZURE_STORAGE_CONTAINER_NAME.
The factory in createProvider() runs at module load time. If neither R2 nor Azure env vars are present, it captures the error rather than throwing at startup. Auth-only routes like /health and /auth/* keep working; the first actual storage operation fails with a clear message instead of the previous behavior, which silently completed uploads without writing any bytes.
One thing to note: after both commits the factory only checks for R2 credentials - Azure is not yet wired into createProvider(). The AzureBlobProvider class is in the file but the selection logic needs to be extended to use it.
Spotted something wrong? Or know the PR text has fresher detail than the writeup above?