Using R2
File uploads, downloads, and role-based access with Cloudflare R2
Cloudflare R2 provides S3-compatible object storage for file uploads in the Quickback Stack.
By default defineFileStorage("cloudflare-r2") is presign-only: you get the
ctx.storage signer and nothing else. The /storage/v1/* endpoints shown
further down only exist in the managed subsystem
(managed: true).
Upload Flow (default — ctx.storage)
Write a normal defineAction that signs a presigned URL and stores the key in
your own table. Bytes go directly to R2 — the Worker only signs.
export default defineAction({
path: "/media/sign-upload",
method: "POST",
input: z.object({ filename: z.string(), contentType: z.string() }),
access: { roles: ["member"] },
async execute({ input, ctx, db, storage }) {
const key = `org/${ctx.activeOrgId}/${crypto.randomUUID()}-${input.filename}`;
const upload = await storage.signPutUrl(key, { contentType: input.contentType, expiresIn: 600 });
await db.insert(media).values({ key /* … */ });
return { uploadUrl: upload.url, key, headers: upload.headers };
},
});# 1. Ask your action for a signed URL (your auth/roles run here)
curl -X POST https://api.example.com/api/v1/media/sign-upload \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{ "filename": "report.pdf", "contentType": "application/pdf" }'
# → { "uploadUrl": "https://<account>.r2.cloudflarestorage.com/...&X-Amz-Signature=...", "key": "...", "headers": { "Content-Type": "application/pdf" } }
# 2. Upload the bytes straight to R2 (replay the returned headers)
curl -X PUT "<uploadUrl>" -H "Content-Type: application/pdf" --data-binary @report.pdfstorage.signPutUrl(key, { contentType?, expiresIn?, bucket? }) returns
{ url, method, headers, key, expiresAt }; storage.signGetUrl(key, { expiresIn?, downloadFilename?, bucket? })
returns a presigned GET URL. Pass bucket to target a bucket other than the
configured default.
Presigning needs an R2 API token — set the R2_ACCOUNT_ID, R2_ACCESS_KEY_ID,
and R2_SECRET_ACCESS_KEY secrets. See R2 Setup → Presigned uploads.
Upload Flow (managed mode — /storage/v1/*)
With managed: true, the generated API also exposes turnkey presigned
endpoints backed by the FILES_DB metadata system:
# 1. Request a presigned upload URL
curl -X POST https://api.example.com/storage/v1/presign/documents/report.pdf \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{ "contentType": "application/pdf", "size": 248000 }'
# → { "id": "obj_abc", "key": "...", "uploadUrl": "...", "headers": {...}, "expiresAt": "..." }
# 2. Upload directly to R2
curl -X PUT "<uploadUrl>" -H "Content-Type: application/pdf" --data-binary @report.pdf
# 3. Confirm (reconciles size + etag)
curl -X POST https://api.example.com/storage/v1/confirm/documents/report.pdf \
-H "Authorization: Bearer <token>"Download Flow
In presign-only mode, mint a short-lived download URL with
storage.signGetUrl(key, { downloadFilename }) from your action and hand it to
the client — the bytes come straight from R2.
In managed mode, files are also served through the worker with security checks applied:
# Download a file (auth + firewall enforced)
curl https://api.example.com/api/v1/files/file_abc123/download \
-H "Authorization: Bearer <token>"The download endpoint:
- Validates the user's session
- Checks firewall — the file must belong to the user's organization
- Checks access — the user must have the required role
- Streams the file from R2
Role-Based Access
File access respects the same security layers as your API:
- Firewall — Users can only access files belonging to their organization
- Access — Role-based download permissions