Azure Blob Storage
Learn how to use your own Azure Blob Storage with EdgeStore.
Use the Azure Blob provider when your files must live in your own Azure storage account. It supports direct browser uploads, automatic block uploads for large files, backend uploads, private signed reads, and stable key references. Your application owns the container's CORS, access, and lifecycle configuration.
The provider creates short-lived, blob-scoped SAS URLs for browser uploads and private reads. The account key stays on the server, and permanent file URLs never contain credentials. Hosted features such as image processing are not available.
Installation
npm install @azure/storage-blobimport { initEdgeStore } from '@edgestore/server';
import { azureBlob } from '@edgestore/server/providers/azure-blob';
const es = initEdgeStore.create();
export const router = es.router({
documents: es.fileBucket().accessControl('private'),
}).provider(
azureBlob({
storageAccountName: 'myaccount',
containerName: 'edgestore',
}),
);
export type EdgeStoreRouter = typeof router;Pass router to your framework adapter as its router option. Set
ES_AZURE_ACCOUNT_KEY on the server, and set EDGESTORE_JWT_SECRET to a
stable random secret, for example one generated with openssl rand -base64 32.
All application instances must share it. Credentials are only checked when the
provider is first used, so defining a router does not require them.
| Option | Default / environment variable |
|---|---|
storageAccountName | ES_AZURE_ACCOUNT_NAME |
storageAccountKey | ES_AZURE_ACCOUNT_KEY |
containerName | ES_AZURE_CONTAINER_NAME |
endpoint | ES_AZURE_ENDPOINT, otherwise https://<account>.blob.core.windows.net |
baseUrl | EDGESTORE_BASE_URL, otherwise <endpoint>/<container> |
uploadUrlExpiresIn | 3600 seconds |
signedUrlExpiresIn | 3600 seconds |
multipart.thresholdBytes | 100 MiB |
multipart.partSizeBytes | 16 MiB; minimum 5 MiB, maximum 4000 MiB |
multipart.sessionExpiresIn | 86400 seconds |
jwtSecret | Multipart signing override; otherwise EDGESTORE_JWT_SECRET or EDGESTORE_SECRET_KEY |
Signed URL lifetimes must be between 1 and 604800 seconds. Upload URLs grant only create/write permission on one blob, and read URLs grant only read permission.
Paths and object settings
Blobs are stored as <router bucket>/<path>. The default path adds _public/
for public buckets, then the router path values and the file name. Use path
to change everything after the router bucket, which is always kept:
azureBlob({
path: ({ defaultPath }) => `tenants/${defaultPath}`,
objectOptions: {
cacheControl: 'private, max-age=3600',
contentDisposition: 'attachment',
metadata: { source: 'edgestore' },
},
});objectOptions can also be a function that receives the same arguments as
path. The content type comes from the uploaded file. Browser and backend
uploads apply the same settings.
Large files and cancellation
Files above the threshold are uploaded as blocks, in parallel, with retries.
The browser requests block upload URLs as it reaches each block, so
uploadUrlExpiresIn only needs to cover one block transfer. A session can
request URLs and complete until multipart.sessionExpiresIn passes. Before
committing, the provider checks that every block exists with the expected size,
which catches incomplete uploads.
Azure has no multipart session to cancel. Blocks that are never committed are discarded automatically after seven days, so canceled and failed uploads need no lifecycle rule.
Backend uploads use the same paths, object settings, and block configuration, and send up to four blocks at a time.
Private files
Use accessControl('private') for private buckets. Cookie-based access-control
rules are not supported and fail at initialization. Create read URLs from the
backend client:
const { signedUrl } = await router.client.documents.createSignedUrl({
url: { key: 'documents/report.pdf' },
expiresIn: 300,
});Add .autoSignedUrls({ expiresIn: 300 }) to a private router bucket to receive
a read URL with each upload response.
Container configuration
Keep the container private. Azure grants anonymous read access to a whole
container, not to a prefix, so public buckets need their own read path.
Public uploads return an unsigned URL under _public/, which a private
container answers with 403. Either:
- serve
_public/through a CDN or proxy that can read the private container, such as Azure Front Door with a private origin, and setbaseUrlto it; or - if the container holds only public buckets, set its public access level to Blob (anonymous reads of individual blobs, no listing). Every file in that container is then readable by URL, so never combine this with private buckets.
Configure blob service CORS for your application origins so browsers can upload:
[{
"AllowedOrigins": ["http://localhost:3000", "https://app.example.com"],
"AllowedMethods": ["PUT", "GET", "HEAD"],
"AllowedHeaders": ["*"],
"ExposedHeaders": ["*"],
"MaxAgeInSeconds": 3600
}]Local development with Azurite
Azurite emulates Blob Storage locally:
docker run -p 10000:10000 mcr.microsoft.com/azure-storage/azurite azurite-blob --blobHost 0.0.0.0azureBlob({
storageAccountName: 'devstoreaccount1',
// Azurite's documented development account key.
storageAccountKey:
'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==',
containerName: 'edgestore',
endpoint: 'http://127.0.0.1:10000/devstoreaccount1',
});Limitations
temporaryandreplaceTargetUrlare rejected. Track application-owned files in your database and delete old files after a successful replacement.- Azure SAS URLs grant write access to one blob and cannot limit its size or
the write operation. A browser holding an upload URL can therefore write
that blob directly, with any size and blob headers. The router's
maxSize, mime-type checks, andobjectOptionsapply to the declared upload and to backend uploads, but are not enforced on bytes a browser sends. If you need those guarantees, validate each stored blob after upload from a storage event, for example an Event GridBlobCreatedsubscription that triggers an Azure Function, and delete blobs that fail your checks. - Confirmation, listing, restore, and generated thumbnails are unsupported.
- Only account-key authentication is supported.