Skip to content

Storage Options

EmDash stores uploaded media (images, documents, videos) in a configurable storage backend. Choose based on your deployment platform and requirements.

StorageBest ForFeatures
R2 BindingCloudflare WorkersZero-config, fast
S3Any platformSigned uploads, CDN support
LocalDevelopmentSimple filesystem storage

Use R2 bindings when deploying to Cloudflare Workers for the fastest integration.

astro.config.mjs
import emdash, { r2 } from "emdash/astro";
export default defineConfig({
integrations: [
emdash({
storage: r2({ binding: "MEDIA" }),
}),
],
});
OptionTypeDescription
bindingstringR2 binding name from wrangler.jsonc
publicUrlstringOptional public URL for the bucket

Add the R2 binding to your Wrangler configuration:

{
"r2_buckets": [
{
"binding": "MEDIA",
"bucket_name": "emdash-media"
}
]
}

For public media URLs, enable public access on your R2 bucket:

  1. Go to Cloudflare Dashboard > R2 > your bucket
  2. Enable public access under Settings
  3. Add the public URL to your config:
storage: r2({
binding: "MEDIA",
publicUrl: "https://pub-xxxx.r2.dev",
});

The S3 adapter works with Cloudflare R2 (via S3 API), MinIO, and other S3-compatible services.

astro.config.mjs
import emdash, { s3 } from "emdash/astro";
export default defineConfig({
integrations: [
emdash({
storage: s3({
endpoint: process.env.S3_ENDPOINT,
bucket: process.env.S3_BUCKET,
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
region: "auto", // Optional, defaults to "auto"
publicUrl: process.env.S3_PUBLIC_URL, // Optional CDN URL
}),
}),
],
});
OptionTypeDescription
endpointstringS3 endpoint URL
bucketstringBucket name
accessKeyIdstringAccess key
secretAccessKeystringSecret key
regionstringRegion (default: "auto")
publicUrlstringOptional CDN or public URL

Use S3 credentials with R2 for features like signed upload URLs:

storage: s3({
endpoint: "https://<account-id>.r2.cloudflarestorage.com",
bucket: "emdash-media",
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
publicUrl: "https://pub-xxxx.r2.dev",
});

Generate R2 API credentials in the Cloudflare dashboard under R2 > Manage R2 API Tokens.

storage: s3({
endpoint: "https://minio.example.com",
bucket: "emdash-media",
accessKeyId: process.env.MINIO_ACCESS_KEY,
secretAccessKey: process.env.MINIO_SECRET_KEY,
publicUrl: "https://minio.example.com/emdash-media",
});

Use local storage for development. Files are stored in a directory on disk.

astro.config.mjs
import emdash, { local } from "emdash/astro";
export default defineConfig({
integrations: [
emdash({
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
}),
],
});
OptionTypeDescription
directorystringDirectory path for file storage
baseUrlstringBase URL for serving files

The baseUrl should match EmDash’s media file endpoint (/_emdash/api/media/file) unless you configure a custom static file server.

Switch storage backends based on environment:

astro.config.mjs
import emdash, { r2, s3, local } from "emdash/astro";
const storage = import.meta.env.PROD
? r2({ binding: "MEDIA" })
: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
});
export default defineConfig({
integrations: [emdash({ storage })],
});

The S3 adapter supports signed upload URLs, allowing clients to upload directly to storage without passing through your server. This improves performance for large files.

Signed uploads are automatic when using the S3 adapter. The admin interface uses them when available.

Adapters that support signed uploads:

  • S3 (including R2 via S3 API)

Adapters that do not support signed uploads:

  • R2 binding (use S3 adapter with R2 credentials instead)
  • Local

All storage adapters implement the same interface:

interface Storage {
upload(options: {
key: string;
body: Buffer | Uint8Array | ReadableStream;
contentType: string;
}): Promise<UploadResult>;
download(key: string): Promise<DownloadResult>;
delete(key: string): Promise<void>;
exists(key: string): Promise<boolean>;
list(options?: ListOptions): Promise<ListResult>;
getSignedUploadUrl(options: SignedUploadOptions): Promise<SignedUploadUrl>;
getPublicUrl(key: string): string;
}

This consistency allows switching storage backends without changing application code.