# How to Configure a Cloudflare R2 Bucket: Public Development URLs vs Custom Domains

> Configure your Cloudflare R2 bucket for development with public URLs and for production using custom domains. Learn how to set CLOUDFLARE_R2_URL in your Next.js app.

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

---

**Use the auto-generated `https://pub-*.r2.dev` URL for local development and a custom domain for production by setting the `CLOUDFLARE_R2_URL` environment variable in your Cloudflare Next.js application.**

The `ifindev/fullstack-next-cloudflare` repository demonstrates a clean pattern for handling file uploads to Cloudflare R2 storage while seamlessly switching between development and production URL schemes. By abstracting the bucket URL behind a single environment variable, you can leverage instant public development URLs during local testing and switch to branded custom domains in production without modifying application code.

## How the R2 URL Architecture Works

The application stores files in a Cloudflare R2 bucket bound to the Worker via `wrangler.jsonc`. During request handling, the helper in [`src/lib/r2.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/lib/r2.ts) obtains the bucket from the Cloudflare environment, uploads the file, and constructs a public URL using the base value from `CLOUDFLARE_R2_URL`.

### The Bucket Binding Configuration

The R2 bucket binding is declared in **`wrangler.jsonc`**, making the bucket available to your Next.js application running on Cloudflare Workers:

```jsonc
// wrangler.jsonc
{
  "r2_buckets": [
    {
      "bucket_name": "next-cf-app-bucket",
      "binding": "next_cf_app_bucket"
    }
  ]
}

```

This binding name (`next_cf_app_bucket`) is how your code accesses the R2 storage API within the Worker environment.

### The URL Construction Logic

In [`src/lib/r2.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/lib/r2.ts), the public URL is constructed by concatenating the environment variable with the file key:

```ts
// src/lib/r2.ts
const { env } = await getCloudflareContext();
const publicUrl = `https://${(env as any).CLOUDFLARE_R2_URL}/${key}`;

```

The value of `CLOUDFLARE_R2_URL` determines whether you serve files from the temporary development endpoint or your branded domain.

## Development vs Production URLs

Cloudflare R2 provides two distinct URL strategies for accessing uploaded files. The repository leverages both through environment-specific configuration.

### Public Development URLs

**Public Development URLs** are auto-generated by Cloudflare when you create an R2 bucket (format: `https://pub-a1b2c3d4e5f6g7h8i9j0.r2.dev`). 

Characteristics:
- **Instantly available** after bucket creation with no DNS configuration
- **Free SSL** provided automatically by Cloudflare
- **Intended for testing only** due to temporary nature and branding

Set this in your local environment file:

```bash

# .dev.vars

CLOUDFLARE_R2_URL=https://pub-a1b2c3d4e5f6g7h8i9j0.r2.dev

```

### Custom Domains

**Custom domains** (e.g., `https://files.yourdomain.com`) require manual configuration in the Cloudflare dashboard but provide production-grade reliability.

Benefits:
- **Brand consistency** with your own hostname
- **CDN-level caching** and optimized edge delivery
- **Custom SSL certificates** and advanced security controls
- **Permanent URLs** suitable for production databases

Configure this as a secret in production:

```bash
echo "https://files.yourdomain.com" | wrangler secret put CLOUDFLARE_R2_URL

```

## Implementation in Code

The `uploadToR2` function handles the complete upload flow and URL generation, remaining agnostic to whether you are using development or production URLs.

### The uploadToR2 Helper

Located in **[`src/lib/r2.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/lib/r2.ts)**, this function generates a unique key, uploads the file buffer, and returns the complete public URL:

```ts
// src/lib/r2.ts
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 ext = file.name.split(".").pop() ?? "bin";
  const key = `${folder}/${timestamp}_${randomId}.${ext}`;

  const arrayBuffer = await file.arrayBuffer();

  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}` 
    },
  });

  const publicUrl = `https://${(env as any).CLOUDFLARE_R2_URL}/${key}`;
  return { success: true, url: publicUrl, key };
}

```

### Consuming the Upload Function

Modules such as the Todo actions import this helper to handle file attachments:

```ts
// src/modules/todos/actions/create-todo.action.ts
import { uploadToR2 } from "@/lib/r2";

export async function createTodoAction(file: File) {
  const uploadResult = await uploadToR2(file);
  if (!uploadResult.success) throw new Error(uploadResult.error);
  // Store uploadResult.url in your database with the todo record
}

```

## Setting Up a Custom Domain

To switch from public development URLs to a custom domain in production:

1. **Open the Cloudflare Dashboard** → R2 → Your Bucket → Custom Domains
2. **Click "Connect Domain"** and enter your desired hostname (e.g., `files.yourdomain.com`)
3. **Follow DNS instructions** to create a CNAME record pointing to your bucket's `.r2.dev` endpoint
4. **Wait for SSL issuance** (usually completes within minutes)
5. **Set the production secret** using Wrangler:

```bash
wrangler secret put CLOUDFLARE_R2_URL

# Enter: https://files.yourdomain.com

```

Once configured, all existing and new files will be accessible via your custom domain without requiring any code changes, as the `uploadToR2` function dynamically reads the URL base from the environment.

## Summary

- **Single environment variable controls the URL scheme**: Set `CLOUDFLARE_R2_URL` to either your public development URL or custom domain
- **No code changes required**: The `uploadToR2` helper in [`src/lib/r2.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/lib/r2.ts) constructs URLs dynamically based on the environment
- **Bucket binding is static**: Defined in `wrangler.jsonc` as `next_cf_app_bucket` and referenced consistently across environments
- **Development uses auto-generated URLs**: Format `https://pub-*.r2.dev` requires zero configuration
- **Production requires custom domain setup**: Configure via Cloudflare dashboard for branded, cached file delivery

## Frequently Asked Questions

### What is the difference between R2 public development URLs and custom domains?

Public development URLs are temporary endpoints automatically generated by Cloudflare when you create an R2 bucket (e.g., `https://pub-xxx.r2.dev`), suitable for local testing but lacking branding and advanced caching. Custom domains are hostnames you configure yourself (e.g., `https://files.example.com`) that provide CDN-level performance, custom SSL, and professional branding required for production applications.

### How do I configure the R2 bucket URL for local development?

Create a `.dev.vars` file in your project root and set `CLOUDFLARE_R2_URL` to your bucket's public development URL: `CLOUDFLARE_R2_URL=https://pub-a1b2c3d4e5f6g7h8i9j0.r2.dev`. The [`src/lib/r2.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/lib/r2.ts) helper automatically reads this variable when running `wrangler dev` or Next.js local development mode.

### Where is the R2 bucket binding configured in the Next.js app?

The binding is declared in **`wrangler.jsonc`** under the `r2_buckets` array with the name `next_cf_app_bucket`. This binding name is referenced in code via the Cloudflare environment object (`env.next_cf_app_bucket`) to execute put, get, and list operations on the bucket.

### Do I need to modify code when switching from development to production URLs?

No. The application code remains identical between environments because the URL construction logic in `uploadToR2` references the `CLOUDFLARE_R2_URL` environment variable. You only need to change the value of this variable via `wrangler secret put` for production or `.dev.vars` for local development, leaving the TypeScript implementation in [`src/lib/r2.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/lib/r2.ts) untouched.