Blob Storage
Some WDCC projects need somewhere to put user-uploaded files - profile pictures, event photos, PDFs.
- Cloudflare R2 - the default for all new projects
- AWS S3 - legacy, plus a few specific cases
Both speak the same API (R2 is S3-compatible), so the code is nearly identical either way. The difference is cost, and who can touch what.
Currently, we should default to using R2 since it is cheaper and easier to use. Some legacy projects are still using S3 which is why we still include it here as an option - Always default to R2 where possible.
Key Links
- R2 Dashboard - all our buckets, under the main WDCC account
- AWS Console - the remaining S3 buckets
- See Project Tracker for which project uses which, plus credentials
| I want to... | Go to |
|---|---|
| Use the bucket I've been given | For project leads |
| Move a project off S3 | Migrating |
Serve files on a wdcc.co.nz domain |
Custom domains |
Convention
Each WDCC project should get one bucket. This is due to R2 API token being scoped to a bucket and we want each secret key to only control one project so that we can hand it out to the leads in isolation.
wdcc-<project> - production (e.g. wdcc-ayo)
wdcc-<project>-staging - staging (e.g. wdcc-ayo-staging)
Setting up a new R2 Bucket
1. Create a new Bucket
Login to Cloudflare > R2 Object Storage > Create Bucket > Name the bucket using the naming convention above > Location: Automatic and Storage Class: Standard
2. Custom domain
Skip this if the files are private (i.e. always served through the app via presigned URLs). Only do it for files meant to be publicly fetchable, like avatars in an <img> tag.
Most projects will require a custom domain.
Bucket > Settings > Custom Domains > Add
The DNS record should be created for you automatically, since we own the wdcc.co.nz domain.
Never serve production files from the *.r2.dev URL.
It's rate-limited and meant for poking at a bucket during development. Use a custom domain.
3. Generate a Scoped Token
R2 Object Storage > Manage API Tokens (bottom right) > Create Account API Token
Token settings
- Name:
<project>-prod(ordev) - Permissions:
Object Read & Write - Specify buckets: select only this project's bucket. Do not leave it on "all buckets"
- TTL: Forever
After creating the token, you will get an Access Key ID, Secret Access Key and the Endpoint. These secrets are shown once so make sure you save it (it should be stored in: TECH DOCUMENT)
4. Hand it over
For projects, they would need the following four values in their env:
S3_ENDPOINT <from step 3>
S3_REGION auto
S3_ACCESS_KEY_ID <from step 3>
S3_SECRET_ACCESS_KEY <from step 3>
Using your bucket (project leads)
You've been given four values. They go in .env locally and in your host's secrets (fly secrets set ...) for deployments - never in the repo.
From code
R2 is S3-compatible, so use the normal AWS SDK. Nothing Cloudflare-specific to install.
import { S3Client } from "@aws-sdk/client-s3";
export const s3 = new S3Client({
region: "auto",
endpoint: process.env.S3_ENDPOINT,
forcePathStyle: true,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
},
});
Two gotchas (according to claude)
forcePathStyle: trueis required. Without it the SDK builds a URL R2 doesn't answer on- If uploads fail with a confusing
400or501, it's the SDK's default checksums. AddrequestChecksumCalculation: "WHEN_REQUIRED"to the client config
Migrating S3 to R2
[TODO]
Notes:
Don't LIST the bucket in a request path.
Listing counts as a Class A op - the expensive tier, and the one with the smallest allowance. An app that lists the bucket to render a gallery on every page load will burn the shared monthly allowance for every other project without uploading a single file.
Store the object keys in your database next to whatever record owns them, and read from there.
AWS (legacy)
We keep the AWS account for the projects still on S3 and for the occasional one-off.
- The account stays open even when empty - old client handover docs reference the account ID
- There's a $5 budget alarm on it. If that fires and nobody deliberately started something, treat it as a possible leaked key and rotate immediately
- New IAM users get one policy scoped to one bucket, and no console access. Access keys go in the Project Tracker with the date issued
A leaked AWS key is a much worse day than a leaked R2 token - AWS keys can start compute in regions nobody checks, R2 tokens can only touch files. That asymmetry is most of why we moved.