# Deploying Next.js SSR on Cloudflare: The Complete Guide to OpenNext vs next-on-pages

---

After you complete this article, you will have a solid understanding of:

* Why deploying Next.js SSR on Cloudflare is different from traditional hosting
    
* The real difference between Cloudflare Pages with next-on-pages and OpenNext adapter
    
* How the free tier's 100,000 daily requests can handle production traffic
    
* Common deployment pitfalls that will waste hours of debugging
    
* Which adapter to choose for your specific use case
    

## Have You Ever Seen This Error When Deploying to Cloudflare?

If you've tried deploying a Next.js app to Cloudflare Pages, you've probably encountered this frustrating message:

```plaintext
Error: Dynamic server usage: Page couldn't be rendered statically because it used `cookies`.
```

Or even worse:

```plaintext
Error: The edge runtime does not support Node.js 'fs' module.
You can use 'fs' module only in Node.js runtime.
```

And then you wonder: "But I thought Cloudflare supports Next.js SSR now?"

![A confused developer looking at two doors - one labeled "Edge Runtime" with a ⚠️ warning sign, another labeled "Node.js Runtime" with a ✅ check mark](https://cdn.hashnode.com/res/hashnode/image/upload/v1753633175139/b8514323-216a-4e88-a9d1-5aab5f5dd3c1.png align="center")

Let me clear up this confusion once and for all.

## The Two Ways to Deploy Next.js on Cloudflare

There are two completely different approaches to deploying Next.js on Cloudflare, and choosing the wrong one will cause endless headaches.

### Option 1: @cloudflare/next-on-pages (The Limited One)

This was the original way, and it comes with a massive limitation:

```typescript
// Every server component needs this 👇
export const runtime = 'edge';

export default async function Page() {
  // ❌ This will fail!
  const fs = require('fs');
  
  // ❌ This will also fail!
  const bcrypt = require('bcrypt');
  
  // ✅ Only Web APIs work
  const response = await fetch('https://api.example.com');
  
  return <div>Limited to Edge Runtime</div>;
}
```

### Option 2: @opennextjs/cloudflare (The Game Changer)

Released in 2024 and now in v1.0-beta, this adapter supports the Node.js runtime:

```typescript
// No runtime declaration needed! 🎉

export default async function Page() {
  // ✅ This works now!
  const fs = require('fs');
  
  // ✅ This works too!
  const crypto = require('crypto');
  
  // ✅ Even database connections work
  const data = await prisma.user.findMany();
  
  return <div>Full Node.js support!</div>;
}
```

## Setting Up Next.js SSR with OpenNext (The Right Way)

Let's deploy a real Next.js app with full SSR support on Cloudflare.

### **Option 1: Using Cloudflare CLI (Recommended)**

```bash
# Create new Cloudflare project
npm create cloudflare@latest my-nextjs-app -- \
  --framework=next --platform=workers

# Deploy
npm run deploy
```

Ready to see this in action? Check out the complete working example with full source code, deployment configuration, and step-by-step setup instructions [**here**](https://github.com/Yusadolat/my-nextjs-app)

---

## Migrating Your Existing Next.js App to Cloudflare

If you already have a Next.js app running on Vercel, AWS, or anywhere else, here's how to migrate it to Cloudflare.

### Step 1: Check Your Current Setup

First, identify which features your app uses:

```bash
# Check your Next.js version
npm list next

# Look for these in your code:
grep -r "export const runtime" . # Edge runtime declarations
grep -r "getServerSideProps" .   # SSR pages
grep -r "getStaticProps" .        # SSG pages
grep -r "app/api" .               # API routes
```

### Step 2: Choose Your Migration Path

**If your app uses Vercel:**

```bash
# Use Diverce for automatic migration!
npx diverce migrate

# This tool automatically:
# - Adds OpenNext to your project
# - Updates your configuration
# - Creates a PR with all changes
```

**For manual migration:**

```bash
# Install the OpenNext adapter
npm install -D @opennextjs/cloudflare wrangler

# Remove any Vercel-specific packages
npm uninstall @vercel/analytics @vercel/og
```

### Step 3: Update Your Configuration

**Remove edge runtime declarations:**

```typescript
// ❌ Remove these from all your files
export const runtime = 'edge';
export const dynamic = 'force-dynamic';

// ✅ OpenNext handles this automatically
// Just delete these lines!
```

**Update your next.config.js:**

```javascript
// next.config.js
const { setupDevPlatform } = require('@cloudflare/next-on-pages/next-dev');

// ❌ Remove this if you have it
if (process.env.NODE_ENV === 'development') {
  await setupDevPlatform();
}

// ✅ Add this instead
import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
initOpenNextCloudflareForDev();

const nextConfig = {
  // Your existing config stays the same!
  images: {
    domains: ['example.com'],
  },
  // Remove any Vercel-specific settings
  // outputFileTracing: false, ❌
};

module.exports = nextConfig;
```

### Step 4: Handle Platform-Specific Code

**If you're using Vercel KV:**

```typescript
// ❌ Before (Vercel KV)
import { kv } from '@vercel/kv';
await kv.set('key', 'value');

// ✅ After (Cloudflare KV)
export async function GET(request: Request, env: Env) {
  await env.MY_KV.put('key', 'value');
  return new Response('Saved!');
}
```

**If you're using Vercel Postgres:**

```typescript
// ❌ Before (Vercel Postgres)
import { sql } from '@vercel/postgres';
const { rows } = await sql`SELECT * FROM users`;

// ✅ After (Any PostgreSQL client works!)
import { Pool } from 'pg';
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});
const { rows } = await pool.query('SELECT * FROM users');
```

**If you're using Vercel Edge Config:**

```typescript
// ❌ Before (Vercel Edge Config)
import { get } from '@vercel/edge-config';
const value = await get('featureFlag');

// ✅ After (Cloudflare KV or D1)
export async function GET(request: Request, env: Env) {
  const value = await env.CONFIG_KV.get('featureFlag');
  return Response.json({ value });
}
```

### Step 5: Update Environment Variables

**Create a** `.dev.vars` file for local development:

```bash
# .dev.vars (like .env.local but for Cloudflare)
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
NEXTAUTH_SECRET=your-secret-key
NEXT_PUBLIC_API_URL=https://api.example.com
```

**Add bindings to wrangler.toml:**  
Before that  
**Optional:** create the R2 bucket for the cache If you added R2 incremental cache in your `open-next.config.ts`.

Run this command to create it:

`npx wrangler r2 bucket create your-bucket-name`

```toml
name = "my-nextjs-app"
main = ".open-next/worker.js"
compatibility_date = "2025-07-27"
compatibility_flags = ["nodejs_compat"]


# Static assets configuration
[assets]
directory = ".open-next/assets"
binding = "ASSETS"


# R2 Buckets
[[r2_buckets]]
binding = "NEXT_INC_CACHE_R2_BUCKET"
bucket_name = "my-bucket-name"
```

## Now let deploy :

```plaintext
//Run the command below
npm run deploy
```

### If everything works well, your Next.js app will be live on Cloudflare's global network within seconds, accessible via a `*.`[`workers.dev`](http://workers.dev) URL or your custom domain, serving from 280+ locations worldwide with automatic SSL.

### Common Migration Issues

**Issue 1: Dynamic imports failing**

```typescript
// ❌ This might fail
const MyComponent = dynamic(() => import('./MyComponent'), {
  ssr: false
});

// ✅ Ensure proper configuration
const MyComponent = dynamic(
  () => import('./MyComponent'),
  { 
    ssr: false,
    loading: () => <div>Loading...</div>
  }
);
```

**Issue 2: File system access**

```typescript
// ❌ This won't work in production
import fs from 'fs';
const data = fs.readFileSync('./data.json');

// ✅ Use static imports or fetch
import data from './data.json';
// OR
const response = await fetch('/data.json');
const data = await response.json();
```

**Issue 3: Image optimization**

```typescript
// ✅ Next/Image works but needs configuration
// In next.config.js
module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './image-loader.js',
  },
};

// image-loader.js
export default function cloudflareLoader({ src, width, quality }) {
  const params = [`width=${width}`];
  if (quality) {
    params.push(`quality=${quality}`);
  }
  const paramsString = params.join(',');
  return `/cdn-cgi/image/${paramsString}/${src}`;
}
```

### Step 7: Test Your Migration

```bash
# 1. Build and preview locally
npm run preview

# 2. Check for errors
# Common errors and fixes:

# "Cannot find module 'fs'"
# → Remove file system operations

# "window is not defined"
# → Wrap in useEffect or check typeof window

# "Module not found: Can't resolve 'encoding'"
# → Add to externals in next.config.js

# 3. Test all your routes
curl http://localhost:8787/api/health
curl http://localhost:8787/dashboard
```

## The Free Tier: More Powerful Than You Think

Cloudflare's free tier includes:

* **100,000 requests per day** (resets at midnight UTC)
    
* **10ms CPU time per request** (plenty for SSR)
    
* **128MB memory per Worker**
    
* **Unlimited static asset requests** 🎉
    

### Real-World Capacity Example

```typescript
// Let's calculate what 100k requests means:

// Average SSR page: ~50ms total time (including I/O)
// CPU time used: ~5-10ms
// Memory used: ~30-50MB

// Daily capacity on free tier:
// - 100,000 page views
// - ~4,166 page views per hour
// - ~69 page views per minute

// That's enough for:
// - A blog with 50k daily visitors (2 pages each)
// - A SaaS dashboard with 10k daily active users
// - An e-commerce site with 20k daily shoppers
```

## Advanced Features That Actually Work

### 1\. API Routes with Full Node.js

```typescript
// app/api/process/route.ts
import { createHash } from 'crypto';
import { headers } from 'next/headers';

export async function POST(request: Request) {
  const body = await request.json();
  
  // ✅ Node.js crypto works!
  const hash = createHash('sha256')
    .update(body.data)
    .digest('hex');
  
  // ✅ Headers manipulation
  const headersList = headers();
  const userAgent = headersList.get('user-agent');
  
  // ✅ Complex processing
  const result = await processDataWithNodeAPIs(body);
  
  return Response.json({ 
    hash, 
    processed: result,
    userAgent 
  });
}
```

### 2\. Middleware That Scales

```typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Runs at the edge for EVERY request
  const country = request.geo?.country || 'US';
  
  // Add custom headers
  const response = NextResponse.next();
  response.headers.set('x-user-country', country);
  
  // Redirect based on geo
  if (country === 'CN' && request.nextUrl.pathname === '/') {
    return NextResponse.redirect(new URL('/cn', request.url));
  }
  
  return response;
}

export const config = {
  matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
};
```

### 3\. ISR That Actually Works

```typescript
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  // Pre-build these pages
  return [
    { slug: 'getting-started' },
    { slug: 'advanced-features' }
  ];
}

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await fetch(`https://api.blog.com/posts/${params.slug}`, {
    next: { revalidate: 3600 } // Revalidate every hour
  });
  
  return <article>{/* Your content */}</article>;
}
```

## Production Deployment Checklist

Before deploying to production, ensure:

```bash
✅ nodejs_compat flag is set
✅ Environment variables are configured in Cloudflare dashboard
✅ R2 bucket is created for caching (optional)
✅ Custom domain is configured
✅ Preview deployments are working
```

### Deploy Command

```bash
# Deploy to production
npm run deploy -- --env production

# Deploy preview
npm run deploy -- --env preview
```

## When to Use Which Approach?

### Use @cloudflare/next-on-pages when:

* Your app only uses Web APIs
    
* You need the absolute fastest cold starts
    
* You're building a simple marketing site
    

### Use @opennextjs/cloudflare when:

* You need Node.js APIs (crypto, fs, etc.)
    
* You're using Prisma or other Node.js ORMs
    
* You have existing Next.js apps to migrate
    
* You want the full Next.js feature set
    

## The Future is Here

With OpenNext adapter reaching v1.0, deploying production Next.js apps on Cloudflare is finally practical. You get:

* **True SSR** with full Node.js support
    
* **Global edge deployment** from 280+ locations
    
* **Generous free tier** for getting started
    
* **Seamless scaling** when you grow
    

Remember: You're not choosing between features and performance anymore. You can have both.

## Was this article helpful for you? If so, let me know what you think in the comment section.
