EdgeStore

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-blob
import { 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.

OptionDefault / environment variable
storageAccountNameES_AZURE_ACCOUNT_NAME
storageAccountKeyES_AZURE_ACCOUNT_KEY
containerNameES_AZURE_CONTAINER_NAME
endpointES_AZURE_ENDPOINT, otherwise https://<account>.blob.core.windows.net
baseUrlEDGESTORE_BASE_URL, otherwise <endpoint>/<container>
uploadUrlExpiresIn3600 seconds
signedUrlExpiresIn3600 seconds
multipart.thresholdBytes100 MiB
multipart.partSizeBytes16 MiB; minimum 5 MiB, maximum 4000 MiB
multipart.sessionExpiresIn86400 seconds
jwtSecretMultipart 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 set baseUrl to 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.0
azureBlob({
  storageAccountName: 'devstoreaccount1',
  // Azurite's documented development account key.
  storageAccountKey:
    'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==',
  containerName: 'edgestore',
  endpoint: 'http://127.0.0.1:10000/devstoreaccount1',
});

Limitations

  • temporary and replaceTargetUrl are 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, and objectOptions apply 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 Grid BlobCreated subscription 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.

On this page