# How to Set Up R2 Object Storage for File Uploads in a Next.js Cloudflare Project

> Learn to set up R2 object storage for file uploads in Next.js with Cloudflare. This guide details bucket creation, wrangler binding, and using helper functions for seamless integration.

- Repository: [Muhammad Arifin/fullstack-next-cloudflare](https://github.com/ifindev/fullstack-next-cloudflare)
- Tags: how-to-guide
- Published: 2026-03-03

---

**To set up R2 object storage in a Next.js Cloudflare project, create an R2 bucket, bind it in `wrangler.jsonc`, configure the `CLOUDFLARE_R2_URL` environment variable, and use the `uploadToR2` helper from [`src/lib/r2.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/lib/r2.ts) to handle file uploads in server actions.**

The ifindev/fullstack-next-cloudflare repository demonstrates a production-ready pattern for integrating Cloudflare R2 with Next.js using the **@opennextjs/cloudflare** adapter. This approach allows you to upload files directly from Next.js server actions without maintaining separate API endpoints, leveraging Cloudflare's edge runtime for low-latency object storage operations.

## Create and Configure Your R2 Bucket

Start by provisioning the storage resource in the Cloudflare dashboard.

1. Navigate to **R2 → Buckets** and click **Create bucket**.
2. Assign a unique name (e.g., `next-cf-app-bucket`).
3. Note the **public URL** displayed as `https://<bucket>.r2.cloudflarestorage.com`. You will store this value as the `CLOUDFLARE_R2_URL` environment variable to construct shareable download links after uploads complete.

## Bind the R2 Bucket to Your Worker

Next, declare the bucket binding in your Worker configuration so the runtime can inject the R2 client into your application.

In `wrangler.jsonc`, locate the `r2_buckets` array and verify the binding matches your bucket name:

```json
"r2_buckets": [
    {
        "bucket_name": "next-cf-app-bucket",
        "binding": "next_cf_app_bucket",
        "preview_bucket_name": "next-cf-app-dev-bucket"
    }
]

```

- The `bucket_name` must match the exact name of your R2 bucket in the Cloudflare dashboard.
- The `binding` name (`next_cf_app_bucket`) is the identifier you will use to access the bucket in code via `env.next_cf_app_bucket`.
- The optional `preview_bucket_name` points to a separate bucket for local development.

If you change the `binding` name here, you must update all references in [`src/lib/r2.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/lib/r2.ts) and any server actions that invoke the bucket.

## Configure Environment Variables

The worker requires the public R2 endpoint to generate accessible URLs for uploaded files. Set this in both local and production environments.

Add the following to `.dev.vars.example` (and your actual `.dev.vars` for local development):

```text
CLOUDFLARE_R2_URL=next-cf-app-bucket.r2.cloudflarestorage.com

```

For production, add the same key-value pair in the Cloudflare dashboard under **Workers & Pages → Environment Variables**. Store sensitive credentials like API keys as **Secrets** rather than plain text variables.

## Implement File Uploads with the R2 Helper

The repository provides a centralized utility in [`src/lib/r2.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/lib/r2.ts) that wraps the R2 binding with key generation, metadata tagging, and URL construction.

The `uploadToR2` function accepts a `File` object and an optional folder path, then executes the upload:

```typescript
export async function uploadToR2(file: File, folder = "uploads"): Promise<UploadResult> {
    const { env } = await getCloudflareContext();
    const timestamp = Date.now();
    const randomId = Math.random().toString(36).substring(2, 15);
    const extension = file.name.split(".").pop() ?? "bin";
    const key = `${folder}/${timestamp}_${randomId}.${extension}`;

    const arrayBuffer = await file.arrayBuffer();

    const result = await env.next_cf_app_bucket.put(key, arrayBuffer, {
        httpMetadata: {
            contentType: file.type,
            cacheControl: "public, max-age=31536000",
        },
        customMetadata: {
            originalName: file.name,
            uploadedAt: new Date().toISOString(),
            size: file.size.toString(),
        },
    });

    const publicUrl = `https://${(env as any).CLOUDFLARE_R2_URL}/${key}`;
    return result ? { success: true, url: publicUrl, key } : { success: false, error: "Upload failed" };
}

```

Key implementation details:
- **Key generation**: Combines a timestamp and random string to prevent collisions and maintain sortability.
- **getCloudflareContext()**: Provided by @opennextjs/cloudflare, this function returns the runtime `env` object containing your R2 binding.
- **Metadata**: Stores the original filename, upload timestamp, and file size as custom metadata for later retrieval.
- **Public URL construction**: Concatenates the `CLOUDFLARE_R2_URL` environment variable with the object key to create a directly accessible link.

## Use R2 Uploads in Server Actions

Integrate the helper into Next.js server actions to handle file uploads from forms. The repository demonstrates this pattern in two Todo-related actions:

- **Creating Todos**: [`src/modules/todos/actions/create-todo.action.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/todos/actions/create-todo.action.ts) extracts an optional image from FormData and uploads it during record creation.
- **Updating Todos**: [`src/modules/todos/actions/update-todo.action.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/todos/actions/update-todo.action.ts) handles replacing existing attachments.

Both actions follow the same flow: extract the `File` from `FormData`, invoke `uploadToR2`, and persist the returned URL in the database.

### Minimal Upload Server Action

Create a reusable server action that accepts FormData and returns the public URL:

```typescript
"use server";

import { uploadToR2 } from "@/lib/r2";

export async function uploadImage(formData: FormData) {
  const file = formData.get("image") as File | null;
  if (!file) throw new Error("No file supplied");

  const result = await uploadToR2(file, "my-uploads");
  if (!result.success) throw new Error(`Upload failed: ${result.error}`);

  // result.url contains the public R2 link
  return result.url;
}

```

### Client Component Integration

Submit files from the browser using a standard form POST:

```tsx
"use client";

export default function ImageUploader() {
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const formData = new FormData(e.target as HTMLFormElement);
    const url = await fetch("/api/upload-image", {
      method: "POST",
      body: formData,
    }).then((res) => res.text());

    alert(`File uploaded! Public URL: ${url}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="file" name="image" accept="image/*" required />
      <button type="submit">Upload</button>
    </form>
  );
}

```

Behind the scenes, the `/api/upload-image` route invokes the `uploadImage` server action shown above.

## Deploy Your Application

Deploy the application to make the R2 binding and environment variables available in the Cloudflare runtime.

- **Development**: Run `wrangler dev` to start a local server with live reloading. The CLI automatically binds your preview bucket and injects environment variables from `.dev.vars`.
- **Production**: Run `wrangler deploy` (or `wrangler publish` on older CLI versions) to push the Worker to Cloudflare's edge network. The `r2_buckets` binding and environment variables configured in the dashboard are automatically attached to the deployment.

Once deployed, any server action calling `uploadToR2` will execute against your live R2 bucket.

## Summary

- **Create an R2 bucket** in the Cloudflare dashboard and copy its public URL for the `CLOUDFLARE_R2_URL` variable.
- **Configure the binding** in `wrangler.jsonc` under the `r2_buckets` array, ensuring the `binding` name matches the code reference `env.next_cf_app_bucket`.
- **Set environment variables** locally in `.dev.vars` and in the Cloudflare dashboard for production.
- **Use the helper function** `uploadToR2` from [`src/lib/r2.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/lib/r2.ts) to handle key generation, metadata, and public URL construction in server actions.
- **Deploy with Wrangler** to inject bindings and variables into the edge runtime.

## Frequently Asked Questions

### What is the difference between the bucket name and the binding name in `wrangler.jsonc`?

The `bucket_name` is the actual identifier of your R2 bucket in the Cloudflare dashboard (e.g., `next-cf-app-bucket`), while the `binding` name (e.g., `next_cf_app_bucket`) is the JavaScript property name used to access the bucket in your code via `env.next_cf_app_bucket`. Changing the binding name requires updating all references in [`src/lib/r2.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/lib/r2.ts) and related server actions.

### Why do I need the `CLOUDFLARE_R2_URL` environment variable if the binding provides access?

The R2 binding provides programmatic access to read and write objects, but it does not expose the public HTTP endpoint of your bucket. The `CLOUDFLARE_R2_URL` variable (e.g., `your-bucket.r2.cloudflarestorage.com`) allows the `uploadToR2` helper to construct shareable HTTPS URLs that clients can use to directly download uploaded files without proxying through your Worker.

### Can I upload files larger than 100MB using this setup?

The `uploadToR2` helper in [`src/lib/r2.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/lib/r2.ts) currently loads the entire file into memory using `file.arrayBuffer()` before uploading. For files larger than a few hundred megabytes, you should implement multipart uploads using the R2 multipart API, or stream the upload directly to avoid memory constraints in the Cloudflare Workers runtime.

### How do I access the R2 bucket in local development versus production?

During local development with `wrangler dev`, the CLI automatically binds the `preview_bucket_name` specified in `wrangler.jsonc` and injects variables from `.dev.vars`. In production, Cloudflare injects the production `bucket_name` and dashboard-configured environment variables. The code remains identical in both environments because `getCloudflareContext()` abstracts the runtime difference.