# How to Switch Between Local Development and Remote Cloudflare Resources in Next.js

> Seamlessly switch between local dev and remote Cloudflare resources in Next.js. Learn to use NODE_ENV to manage R2, D1, and secrets across environments with the same API.

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

---

**You switch between local development and remote Cloudflare resources by using `process.env.NODE_ENV` to conditionally initialize the OpenNext dev runtime locally, while relying on Cloudflare's native `env` bindings in production, with both environments using the same `getCloudflareContext()` API to access R2, D1, and secrets.**

The `ifindev/fullstack-next-cloudflare` repository demonstrates a modern approach to building full-stack Next.js applications that run on Cloudflare Workers via the OpenNext integration. When you need to switch between local development and remote Cloudflare resources, the codebase uses environment detection and runtime initialization to provide a seamless developer experience without code duplication.

## How the Environment Switch Works

The application detects its runtime environment through `process.env.NODE_ENV` and the presence of Cloudflare-specific bindings. This dual-mode architecture ensures that your R2 uploads, D1 database queries, and authentication logic function identically whether you are running `npm run dev` locally or deployed to the edge.

### Local Development Mode

During local development, the system initializes a Worker-compatible runtime that mimics Cloudflare's production environment. In [`next.config.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/next.config.ts), the code checks for `process.env.NODE_ENV === "development"` and calls `initOpenNextCloudflareForDev()` from the `@opennextjs/cloudflare` package. This sets up a local server that supports the same `env` bindings you will use in production.

The local runtime loads environment variables from a `.dev.vars` file (copied from `.dev.vars.example`), which provides credentials for your Cloudflare Account ID, D1 tokens, R2 URLs, and authentication secrets. These variables populate the `env` object accessed via `getCloudflareContext()` in files like [`src/lib/r2.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/lib/r2.ts) and [`src/modules/auth/utils/auth-utils.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/auth/utils/auth-utils.ts), allowing you to test R2 uploads and D1 queries without deploying.

### Remote Production Mode

When you deploy using `wrangler deploy`, the application runs on actual Cloudflare Workers. In this mode, `process.env.NODE_ENV` is not set to `"development"`, so `initOpenNextCloudflareForDev()` is never invoked. Instead, Cloudflare automatically injects the bindings defined in `wrangler.jsonc`—including the R2 bucket `next_cf_app_bucket`, the D1 database `next_cf_app`, and your secrets—directly into the Worker's `env` object.

The same `getCloudflareContext()` calls used during local development now resolve to these production bindings. For client-side authentication, the `authClient` in [`src/modules/auth/utils/auth-client.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/auth/utils/auth-client.ts) dynamically switches its `baseURL` from `http://localhost:3000` to `window.location.origin` when running in production.

## Configuration Files That Control the Switch

### next.config.ts

The [`next.config.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/next.config.ts) file serves as the primary toggle for the development environment. When `NODE_ENV` equals `"development"`, it initializes the OpenNext Cloudflare dev runtime.

```typescript
import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";

/** @type {import("next").NextConfig} */
const nextConfig = { /* … other config … */ };

if (process.env.NODE_ENV === "development") {
  // Starts a local Worker‑compatible dev server.
  initOpenNextCloudflareForDev();
}

export default nextConfig;

```

### .dev.vars and wrangler.jsonc

Local environment variables live in `.dev.vars`, which is loaded by [`drizzle.config.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/drizzle.config.ts) for database migrations and by the OpenNext dev server for runtime access. In production, `wrangler.jsonc` declares the same variable names as Cloudflare bindings, ensuring parity between environments.

```dotenv

# .dev.vars (copy from .dev.vars.example)

NEXTJS_ENV=development
CLOUDFLARE_ACCOUNT_ID=your-account-id
CLOUDFLARE_D1_TOKEN=your-d1-token
CLOUDFLARE_R2_URL=your-r2-public-url
BETTER_AUTH_SECRET=your-auth-secret
GOOGLE_CLIENT_ID=your-google-id
GOOGLE_CLIENT_SECRET=your-google-secret

```

## Code Patterns for Environment-Agnostic Resource Access

### Accessing R2 Buckets and D1 Databases

Business logic files like [`src/lib/r2.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/lib/r2.ts) and [`src/db/index.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/db/index.ts) use `getCloudflareContext()` to obtain environment bindings. This function works identically in both local and production modes, returning an `env` object containing your R2 bucket, D1 database, and configured secrets.

```typescript
import { getCloudflareContext } from "@opennextjs/cloudflare";

export async function uploadToR2(file: File) {
  const { env } = await getCloudflareContext(); // env works locally & in production
  const key = `uploads/${Date.now()}_${file.name}`;
  await env.next_cf_app_bucket.put(key, await file.arrayBuffer(), {
    httpMetadata: { contentType: file.type },
  });
  return `https://${(env as any).CLOUDFLARE_R2_URL}/${key}`;
}

```

### Auth Client URL Switching

The authentication client configuration in [`src/modules/auth/utils/auth-client.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/auth/utils/auth-client.ts) demonstrates runtime-specific URL resolution. The code checks `process.env.NODE_ENV` to determine whether to target the local dev server or the deployed Worker URL.

```typescript
export const authClient = createAuthClient({
  baseURL:
    process.env.NODE_ENV === "development"
      ? "http://localhost:3000"
      : typeof window !== "undefined"
        ? window.location.origin
        : "",
});

```

## Summary

- Use `process.env.NODE_ENV` in [`next.config.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/next.config.ts) to conditionally call `initOpenNextCloudflareForDev()` for local Worker simulation.
- Store local credentials in `.dev.vars` to populate the same `env` bindings that Cloudflare provides in production.
- Access Cloudflare resources through `getCloudflareContext()` in [`src/lib/r2.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/lib/r2.ts), [`src/db/index.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/db/index.ts), and auth utilities to ensure zero code duplication between environments.
- Configure client-side API calls in [`src/modules/auth/utils/auth-client.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/auth/utils/auth-client.ts) to switch base URLs based on the runtime environment.

## Frequently Asked Questions

### How does the OpenNext dev initializer mimic Cloudflare Workers locally?

The `initOpenNextCloudflareForDev()` function from `@opennextjs/cloudflare` starts a local development server that replicates the Cloudflare Workers runtime environment. It intercepts calls to `getCloudflareContext()` and provides the values from `.dev.vars` as if they were native Cloudflare bindings, allowing you to test R2 uploads, D1 queries, and secret access without deploying to the edge.

### What is the difference between .env and .dev.vars in this setup?

In this architecture, `.dev.vars` is specifically designed for Cloudflare Workers compatibility and is loaded by both `wrangler dev` and the OpenNext local runtime. While traditional `.env` files work for standard Node.js processes, `.dev.vars` ensures that environment variables are available through the `env` object in `getCloudflareContext()`, matching the production binding behavior exactly.

### Do I need to modify code when deploying from local to production?

No. The design intentionally uses the same business logic files—such as [`src/lib/r2.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/lib/r2.ts) and [`src/modules/auth/utils/auth-utils.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/auth/utils/auth-utils.ts)—for both environments. The only changes required are environment-specific configurations: using `.dev.vars` locally versus Cloudflare dashboard bindings in production, and letting `process.env.NODE_ENV` automatically control the dev runtime initialization.

### Which Cloudflare resources can I access using getCloudflareContext()?

According to the `wrangler.jsonc` configuration in this repository, you can access the R2 bucket named `next_cf_app_bucket`, the D1 database `next_cf_app`, AI bindings, and any secrets configured in your Cloudflare dashboard. The `getCloudflareContext()` function returns these as properties on the `env` object, accessible in both local development and remote production environments.