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

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

"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 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):

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

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:

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:

"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:

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

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →