Quickback Docs

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.pdf

storage.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:

  1. Validates the user's session
  2. Checks firewall — the file must belong to the user's organization
  3. Checks access — the user must have the required role
  4. 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

See Also

  • R2 Setup — Bucket creation, wrangler bindings, and configuration
  • Avatars — Avatar upload UI integration

On this page