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).

infrastructureintegration

Maintained by Allen Morgan · verified on MikeWatch

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.

So what The `StorageProvider` interface is small and cheap to adopt as a foundation if you want to add a second storage target. The Azure provider is only worth pulling if Azure Blob is on your list. Check that `createProvider()` actually selects the Azure provider before shipping - the diff shows the class added but the factory wiring incomplete.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

2 commits from Altien/mikeOssAzure, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
45310480 refactor(storage): extract StorageProvider interface and R2Provider Allen Morgan 2026-05-08 ↗ GitHub
commit body
Introduces a StorageProvider interface, an R2Provider class wrapping the
existing R2/S3 logic, and a createProvider() factory that selects based
on env vars. The four public functions (uploadFile, downloadFile,
deleteFile, getSignedUrl) delegate to a singleton.

Zero behaviour change for existing R2 deployments. Makes it
straightforward for self-hosters to add other providers (GCS, S3,
local filesystem, Azure Blob) without touching call sites.
c3daf8fa feat(storage): Azure Blob Storage provider Allen Morgan 2026-05-08 ↗ GitHub
commit body
Adds AzureBlobProvider implementing the StorageProvider interface, with
auth via either AZURE_STORAGE_CONNECTION_STRING (local dev / Azurite)
or AZURE_STORAGE_ACCOUNT_NAME + DefaultAzureCredential (Managed Identity
in Container Apps). Container defaults to "documents".

signedUrl() returns null - Azure deployments use the backend download
proxy at GET /download/:token rather than direct storage URLs. The /url
route already falls back to buildDownloadUrl() when this returns null.

The factory now picks Azure when AZURE_STORAGE_* is set, falling back to
R2 when R2_* is set. With neither, the server still starts and the first
storage operation fails with a clear error.

Adds @azure/storage-blob; @azure/identity is shared with lib/config.ts.

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-191.md from inside the repo you want the changes in.

⬇ Download capture-thread-191.md