Cloudflare R2

Upload directly to R2 through its S3-compatible API.

"use client"

import { MiB, r2Adapter } from "@uploadcn/core"

import { demoTransport } from "@/examples/_demo"
import { FileUpload } from "@/components/file-upload"

/**
 * R2 speaks the S3 API: the browser code is the same, the server route
 * uses `r2Storage({ accountId, bucket, … })`. Multipart kicks in at 32 MB.
 */
const adapter = r2Adapter({
  endpoint: "/api/upload",
  transport: demoTransport, // docs only, see examples/_demo.ts
  multipart: { threshold: 32 * MiB, partSize: 8 * MiB, concurrency: 4 },
})

export default function R2PresignedExample() {
  return (
    <FileUpload adapter={adapter} title="Upload directly to Cloudflare R2" />
  )
}

R2 implements the S3 API, so the browser code is identical to S3, r2Adapter is an alias of s3Adapter. The difference is on the server.

Server

app/api/upload/route.ts
import { createUploadRoute, r2Storage } from "@uploadcn/server"

export const { POST } = createUploadRoute({
  storage: r2Storage({
    accountId: process.env.R2_ACCOUNT_ID!,
    bucket: process.env.R2_BUCKET!,
    accessKeyId: process.env.R2_ACCESS_KEY_ID!,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
    // A custom domain or r2.dev URL for reading objects:
    publicUrl: process.env.R2_PUBLIC_URL,
  }),
})

r2Storage uses https://<account>.r2.cloudflarestorage.com, region auto and path-style URLs. For jurisdiction-restricted buckets pass jurisdiction: "eu".

@uploadcn/server signs with Web Crypto, so the same route runs on Cloudflare Workers.

CORS

In the R2 dashboard, under Settings → CORS policy:

[
  {
    "AllowedOrigins": ["https://your-app.com"],
    "AllowedMethods": ["PUT"],
    "AllowedHeaders": ["content-type"],
    "ExposeHeaders": ["ETag"]
  }
]

Client

import { r2Adapter } from "@uploadcn/core"

const adapter = r2Adapter({ endpoint: "/api/upload" })

On this page