Upload files to Cloudflare R2
Cloudflare R2 speaks the S3 API with zero egress fees, which makes it a common
landing spot for user uploads that later get served back to those same users.
For upup it is an ordinary S3-compatible backend with two quirks worth knowing
up front: the endpoint is scoped to your account ID, and the region is the
literal string auto.
This page covers server mode, where @upupjs/server
holds the credentials. In client mode your own endpoint signs the URLs and the
browser PUTs straight to R2 — see
Client Mode vs Server Mode for the split.
The config
Only the storage block differs from the
AWS S3 example — keep uploadTokenSecret and the
rest of the handler as they are there.
const storage = {
type: 'r2',
bucket: process.env.R2_BUCKET!,
region: 'auto',
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
}region: 'auto' is not a placeholder to fill in. R2 has no per-region S3
hostnames — a bucket's location hint (Western North America, Europe, and so on)
affects where data lives, not how you sign for it, and the S3 API expects auto
regardless. Passing a real AWS region here produces a signature error.
Console setup
- Create the bucket. Cloudflare dashboard → R2 → Create bucket. A location hint is optional; it does not change the config above.
- Copy your account ID. It is on the R2 overview page and is the first label in the endpoint hostname.
- Create an API token. R2 → Manage R2 API Tokens → Create API token, with Object Read & Write permission. Scope it to the one bucket rather than the whole account. Cloudflare shows the access key ID, the secret access key, and the endpoint once — the secret is not retrievable later.
- Add a CORS policy (client mode only) under the bucket's Settings tab.
CORS (client mode only)
[
{
"AllowedOrigins": ["https://your-app.com"],
"AllowedMethods": ["PUT", "POST", "GET", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]In server mode the browser never contacts R2, so the bucket needs no CORS policy at all. See Credentials And CORS.
R2 multipart parts must be equal-sized
R2 requires every part of a multipart upload except the last to be exactly
the same size — S3 itself only enforces a 5 MiB minimum. upup's multipart
strategy already slices files into uniform parts (5 MiB by default), so the
default path is safe. If you override resumable.chunkSizeBytes, keep it a
single fixed value rather than computing a per-file or per-part size.
Serving the files back
R2 buckets are private by default and the config above never makes them public.
Two supported ways to serve an uploaded object: attach a custom domain to the
bucket (the production answer, and it puts Cloudflare's cache in front of your
files), or enable the r2.dev development subdomain, which is rate-limited and
explicitly not meant for production traffic. Either is a Cloudflare-side setting
and has no effect on the upload path.
R2 does not implement object ACLs. Anything that would be a per-object
public-read on S3 is a bucket-level decision here instead.
Next steps
- Storage Providers — the provider matrix and
the shared
storageconfig reference. - Server Mode — Setup —
getUserId,providers,tokenStore, and tuning. - Client Mode vs Server Mode — which one you want and what changes on the wire.