Upload and serve files through a unified API. The storage adapter is selected by STORAGE_PROVIDER.
Providers
| Provider | Env Value | Best For |
|---|---|---|
| Local filesystem | local (default) | Development |
| Amazon S3 | s3 | Production (AWS) |
| Cloudflare R2 | r2 | Production (Cloudflare) |
Upload API
POST /api/upload accepts multipart form data. Authentication is required.
const form = new FormData();
form.append('file', file);
const res = await fetch('/api/upload', {
method: 'POST',
body: form,
});
const { url } = await res.json();
Image uploads are automatically optimized (resized, compressed) before storage.
Switching to S3 or R2
- Install the AWS SDK:
pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
- Set the provider and credentials in
.env.local:
STORAGE_PROVIDER="s3" # or "r2"
S3_BUCKET="my-bucket"
S3_REGION="us-east-1" # use "auto" for R2
S3_ENDPOINT="" # required for R2 (e.g. https://<account>.r2.cloudflarestorage.com)
S3_ACCESS_KEY_ID="..."
S3_SECRET_ACCESS_KEY="..."
S3_PUBLIC_URL="" # optional — public URL prefix for uploaded files
Production: Always use S3 or R2. The local adapter stores files on disk and is not suitable for multi-instance deployments.
Programmatic Usage
import { uploadFile, deleteFile } from '@/lib/storage';
const url = await uploadFile('avatars/user-123.webp', buffer, 'image/webp');
await deleteFile('avatars/user-123.webp');
Environment Variables
| Variable | Required | Description |
|---|---|---|
STORAGE_PROVIDER | No | "local" (default), "s3", or "r2" |
S3_BUCKET | Yes* | S3 bucket name |
S3_REGION | No | AWS region. Defaults to "auto" (required for R2) |
S3_ENDPOINT | Yes** | Custom S3 endpoint URL (required for R2 and S3-compatible providers) |
S3_ACCESS_KEY_ID | Yes* | Access key ID |
S3_SECRET_ACCESS_KEY | Yes* | Secret access key |
S3_PUBLIC_URL | No | Public URL prefix for uploaded files (e.g. CDN domain) |
* Required when STORAGE_PROVIDER=s3 or r2.
** Required for Cloudflare R2 and other S3-compatible providers.