strapi-provider-upload-azure-blob
v1.0.1
Published
Strapi v5 upload provider for Azure Blob Storage: every auth method, private containers with signed URLs, CDN support.
Maintainers
Readme
strapi-provider-upload-azure-blob
Azure Blob Storage upload provider for Strapi v5.
- Every Azure auth method: account key, connection string, SAS token, managed identity, workload identity, service principal (secret or certificate),
DefaultAzureCredential, or your ownTokenCredential. - Private containers: Strapi serves short-lived, read-only signed URLs. You don't need anonymous access or a CDN.
- CDN / Azure Front Door URLs, with or without the container in the path.
- Follows the Strapi provider contract:
upload()accepts a buffer anduploadStream()accepts a stream, so plugins that re-upload files (image converters, migrations) work. - Safe deletes: deleting a file whose blob is already gone is not an error, and each blob is found from stored metadata, so changing
basePathlater never orphans old files. - Validates config at startup with clear error messages, ships TypeScript types, and is tested against Azurite and a real storage account.
Requires Node.js 22+ and Strapi 5.
Install
npm install strapi-provider-upload-azure-blob
# or
yarn add strapi-provider-upload-azure-blobQuick start
config/plugins.ts:
export default ({ env }) => ({
upload: {
config: {
provider: 'strapi-provider-upload-azure-blob',
providerOptions: {
account: env('AZURE_STORAGE_ACCOUNT'),
containerName: env('AZURE_STORAGE_CONTAINER'),
auth: { type: 'accountKey', accountKey: env('AZURE_STORAGE_ACCOUNT_KEY') },
basePath: 'uploads',
cdnBaseURL: env('AZURE_CDN_BASE_URL'),
privateContainer: env.bool('AZURE_PRIVATE_CONTAINER', false),
cacheControl: 'public, max-age=31536000, immutable',
},
},
},
});Then allow the media host in the admin's Content Security Policy (see Admin previews).
Options
| Option | Type | Default | Description |
|---|---|---|---|
| containerName | string | required | Container that holds the uploads. |
| auth | object | required | How to sign in to Azure. See Authentication. |
| account | string | — | Storage account name. Required for accountKey; otherwise needed unless serviceBaseURL or a connection string identifies the account. |
| serviceBaseURL | string | https://<account>.blob.core.windows.net | Override for Azurite, sovereign clouds or private endpoints. |
| basePath | string | '' | Folder prefix for every blob, e.g. uploads. |
| cdnBaseURL | string | — | Public host in front of the account (Azure Front Door, CDN, custom domain). Stored file URLs use this host. |
| cdnPathIncludesContainer | boolean | true | true: https://cdn/<container>/<blob>. false: https://cdn/<blob>, when the CDN route already points at the container. |
| privateContainer | boolean | false | Serve files through signed URLs. See Private containers. |
| signedUrlExpiresIn | number | 900 | Signed URL lifetime in seconds. |
| cacheControl | string | — | Cache-Control header stored on every blob. |
| createContainerIfNotExists | boolean | false | Create the container on first upload. |
| containerAccessLevel | 'private' \| 'blob' \| 'container' | 'private' | Access level used only when this provider creates the container. |
| uploadOptions.bufferSize | number | 4194304 | Block size in bytes for parallel uploads. |
| uploadOptions.maxConcurrency | number | 5 | Blocks uploaded in parallel. |
Boolean and number options also accept strings such as 'true' and '900', so raw env() values work.
Per-call options set in Strapi's actionOptions.upload / actionOptions.uploadStream are passed to Azure: blobHTTPHeaders, metadata, tags, tier.
Authentication
| auth.type | Fields | Use when |
|---|---|---|
| accountKey | accountKey (+ top-level account) | Simplest setup with a storage account key. |
| connectionString | connectionString | You already have a connection string. The account is read from it. |
| sasToken | sasToken | Limited-permission access. The token needs r, w, c and d on blob objects. |
| managedIdentity | clientId? | Strapi runs on Azure (App Service, Container Apps, VM). Omit clientId for a system-assigned identity. |
| workloadIdentity | clientId?, tenantId?, tokenFilePath? | AKS with workload identity. Defaults come from the standard AZURE_* env vars. |
| servicePrincipal | tenantId, clientId, clientSecret | App registration with a client secret. |
| clientCertificate | tenantId, clientId, certificatePath, certificatePassword? | App registration with a certificate. |
| defaultAzureCredential | managedIdentityClientId?, tenantId? | Let the Azure SDK pick: env vars, workload identity, managed identity, Azure CLI, and so on. Good for local development with az login. |
| tokenCredential | credential | Any TokenCredential instance you construct yourself. |
Identities (every type except accountKey, connectionString and sasToken) need the Storage Blob Data Contributor role on the container or account. That role also allows creating user delegation keys for private mode.
// Managed identity, no secrets in env
auth: { type: 'managedIdentity', clientId: env('AZURE_CLIENT_ID') }
// Service principal
auth: {
type: 'servicePrincipal',
tenantId: env('AZURE_TENANT_ID'),
clientId: env('AZURE_CLIENT_ID'),
clientSecret: env('AZURE_CLIENT_SECRET'),
}Private containers
With privateContainer: true, Strapi keeps the plain URL in the database and signs it each time it returns a file, both in the admin and in the Content API. Each signed URL:
- is read-only (
sp=r) and covers one blob, - expires after
signedUrlExpiresInseconds, - uses the CDN host when
cdnBaseURLis set, so the CDN must forward query strings.
How URLs are signed depends on the auth type:
| Auth | Signing |
|---|---|
| accountKey, connectionString with a key | Service SAS signed with the account key. |
| Any identity-based type | User delegation SAS. One delegation key is cached for 24 hours. |
| sasToken | The configured token is appended as-is, so its own permissions and expiry apply. |
If your frontend caches pages (SSG/ISR) longer than signedUrlExpiresIn, image URLs on those pages will expire. Either raise signedUrlExpiresIn or keep the container public behind a CDN.
CDN / Azure Front Door
Set cdnBaseURL to the public host. Stored URLs then look like:
cdnPathIncludesContainer: true -> https://media.example.com/<container>/uploads/photo_abc123.jpg
cdnPathIncludesContainer: false -> https://media.example.com/uploads/photo_abc123.jpgTrailing slashes in cdnBaseURL and serviceBaseURL are removed automatically.
Admin previews (CSP)
The Media Library loads images from your storage or CDN host, so add that host to Strapi's security middleware in config/middlewares.ts:
const mediaHost = process.env.AZURE_CDN_BASE_URL || `https://${process.env.AZURE_STORAGE_ACCOUNT}.blob.core.windows.net`;
export default [
'strapi::logger',
'strapi::errors',
{
name: 'strapi::security',
config: {
contentSecurityPolicy: {
useDefaults: true,
directives: {
'img-src': ["'self'", 'data:', 'blob:', 'market-assets.strapi.io', mediaHost],
'media-src': ["'self'", 'data:', 'blob:', mediaHost],
},
},
},
},
// ...the rest of the default middlewares
];How files are stored
Blob name: <basePath>/<file.path>/<hash><ext>, with empty parts and duplicate slashes removed. The provider saves { container, blobName } in each file's provider_metadata, including thumbnails and other formats, and uses it to delete and sign. Files without metadata, such as those uploaded by another provider, are found from their URL instead.
Migrating from strapi-provider-upload-azure-storage
| Old option | New option |
|---|---|
| authType: 'default' + accountKey | auth: { type: 'accountKey', accountKey } |
| authType: 'default' + sasToken | auth: { type: 'sasToken', sasToken } |
| authType: 'msi' + clientId | auth: { type: 'defaultAzureCredential', managedIdentityClientId: clientId } (same behaviour), or managedIdentity to use only the managed identity |
| defaultPath | basePath |
| cdnBaseURL | cdnBaseURL |
| removeCN: 'true' | cdnPathIncludesContainer: false |
| defaultCacheControl | cacheControl |
| createContainerIfNotExist | createContainerIfNotExists |
| publicAccessType | containerAccessLevel |
| uploadOptions.maxBuffers | uploadOptions.maxConcurrency |
Set basePath to your old defaultPath so new uploads land in the same folder.
Then update the provider name on existing files. Strapi stores the provider name on every file and only deletes or signs files whose name matches the provider that is currently configured. Files uploaded by the old package are otherwise skipped silently: deleting them in the Media Library leaves the blob behind, and private mode doesn't sign their URLs. Back up the database, then run:
UPDATE files
SET provider = 'strapi-provider-upload-azure-blob'
WHERE provider = 'strapi-provider-upload-azure-storage';After that, old files work like new ones: their URLs already point at the same blobs, and the provider recovers each blob name from the URL.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Media URLs return 409 PublicAccessNotPermitted | The storage account blocks anonymous access. | Use privateContainer: true, put a CDN in front, or allow anonymous blob access on the account and set the container to blob. |
| Upload fails with ContainerNotFound | The container doesn't exist. | Create it, or set createContainerIfNotExists: true. |
| 403 AuthorizationPermissionMismatch | The identity lacks data-plane permission. | Assign Storage Blob Data Contributor. Management roles such as Owner are not enough. |
| Images missing in the admin only | CSP blocks the media host. | See Admin previews. |
| Startup error starting with [strapi-provider-upload-azure-blob] | Invalid or missing option. | The message names the field to fix. |
Development
yarn install
yarn typecheck
yarn test # starts Azurite automatically on port 10010
yarn buildLicense
MIT
