# Using 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](/platform/storage/r2#two-modes)
(`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.

```ts title="quickback/features/media/actions/signUpload.ts"
import { z } from "zod";
import { defineAction, media } from "../.quickback/define-action";

export default defineAction({
  description: "Sign a direct-to-R2 upload URL and record the object key on the media row.",
  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 };
  },
});
```

```bash
# 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](/platform/storage/r2#presigned-uploads-recommended).

## Upload Flow (managed mode — `/storage/v1/*`)

With `managed: true`, the generated API also exposes turnkey presigned
endpoints backed by the FILES_DB metadata system:

```bash
# 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:

```bash
# 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](/platform/storage/r2) — Bucket creation, wrangler bindings, and configuration
- [Avatars](/ui/account/features/avatars) — Avatar upload UI integration
