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

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, 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 and 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 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 file serves as the primary toggle for the development environment. When NODE_ENV equals "development", it initializes the OpenNext Cloudflare dev runtime.

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


# .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 and 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.

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

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 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, 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 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 and 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.

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 →