Skip to content

NextAuth.js with Cloudflare D1

Last Updated: October 27, 2025
Status: ✅ Production-ready
Runtime: Cloudflare Workers with OpenNext adapter

Overview

Authentication is implemented using NextAuth.js 4.24.8 with a custom Prisma adapter for Cloudflare D1. The system supports credentials login, OAuth (Google), and email magic links.

Key Features

  • D1 Database: Session storage in Cloudflare D1 (SQLite)
  • Cloudflare Workers: Compatible with edge runtime
  • Secure Cookies: __Secure- prefix in production, httpOnly, sameSite protection
  • JWT + Sessions: Hybrid approach for optimal performance
  • Multi-provider: Credentials, Google OAuth, Email magic links

Architecture

User Request → NextAuth API Route → Prisma (D1 Adapter) → Cloudflare D1
Session Cookie (JWT) → Server Components → getSession() → D1 Query

Core Components

1. Prisma Client with D1 Adapter

File: src/server/prisma.ts

import { PrismaD1 } from "@prisma/adapter-d1";

function getPrismaClient(db?: D1Database): PrismaClient {
  if (db) {
    // Cloudflare runtime: use D1 adapter
    const adapter = new PrismaD1(db);
    return new PrismaClient({ adapter });
  }
  
  // Local development: direct SQLite connection
  return new PrismaClient();
}

Key Points: - Conditional adapter based on runtime environment - D1 binding passed from Cloudflare context - Falls back to direct connection for local dev

2. NextAuth Configuration

File: src/auth.ts

import { getCloudflareContext } from "@opennextjs/cloudflare";
import { PrismaAdapter } from "@auth/prisma-adapter";

export async function authOptionsWrapper() {
  const { env } = await getCloudflareContext();
  const prisma = getPrismaClient(env.DB);
  const adapter = PrismaAdapter(prisma);

  return {
    adapter,
    secret: process.env.NEXTAUTH_SECRET,
    providers: [/* credentials, google, email */],
    callbacks: {/* jwt, session */},
    events: {/* signIn, signOut */},
  };
}

Key Points: - Dynamic configuration per request - Retrieves D1 binding from Cloudflare context - Adapter injected into NextAuth options

3. Session Retrieval

File: src/auth.ts

export async function getSession() {
  const { env } = await getCloudflareContext();
  const prisma = getPrismaClient(env.DB);
  const adapter = PrismaAdapter(prisma);
  
  return getServerSession({
    ...adapterOptions,
    adapter, // Required to fetch from D1
  });
}

Usage in Server Components:

import { getSession } from "@/auth";

export default async function ProfilePage() {
  const session = await getSession();
  // ...
}

Authentication Providers

1. Credentials Provider

File: src/lib/nextauth/providers.ts

  • Username/password authentication
  • Bcrypt password hashing
  • Creates database session via adapter
  • Returns user with role and organization data

2. Google OAuth

File: src/lib/nextauth/providers.ts

  • OAuth 2.0 flow
  • JWT-based sessions (no database session for OAuth)
  • Profile data synchronized to database
  • Auto-creates user account on first login

File: src/lib/nextauth/custom-email-provider.ts

  • Custom provider using Brevo HTTP API
  • Replaces NextAuth's default SMTP provider
  • 24-hour expiration
  • HTML + text email templates
  • See Email System for details

Security Implementation

File: src/lib/nextauth/callbacks.ts

const isSecure = process.env.NODE_ENV === "production";
const cookieName = isSecure
  ? "__Secure-next-auth.session-token"  // Cloudflare requires
  : "next-auth.session-token";

cookieStore.set(cookieName, sessionToken, {
  httpOnly: true,      // Prevent XSS
  secure: isSecure,    // HTTPS only in production
  sameSite: "lax",     // CSRF protection
  path: "/",           // Global scope
  expires: sessionExpiry,
});

Security Features: - __Secure- prefix enforced by Cloudflare in production - httpOnly prevents JavaScript access - sameSite protects against CSRF - Secure flag requires HTTPS

JWT Configuration

File: src/lib/nextauth/jwt.ts

  • Custom encode/decode for session tokens
  • Consistent cookie naming with callbacks
  • Encryption via NEXTAUTH_SECRET
  • 30-day default expiration

Environment Variable Validation

File: src/lib/nextauth/adapterOptions.ts

if (!process.env.NEXTAUTH_SECRET) {
  throw new Error(
    "NEXTAUTH_SECRET is not set. Configure it in Cloudflare secrets.\n" +
    "wrangler secret put NEXTAUTH_SECRET"
  );
}

Database Schema

NextAuth requires these tables in D1:

model User {
  id            String    @id @default(cuid())
  email         String?   @unique
  emailVerified DateTime?
  name          String?
  image         String?
  role          String    @default("user")
  // ... additional fields
  accounts      Account[]
  sessions      Session[]
}

model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  // OAuth tokens
  user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)
  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime
  @@unique([identifier, token])
}

Environment Configuration

Required Secrets (Cloudflare)

# Set via wrangler CLI
wrangler secret put NEXTAUTH_SECRET
wrangler secret put GOOGLE_CLIENT_ID
wrangler secret put GOOGLE_CLIENT_SECRET
wrangler secret put BREVO_API_KEY

Required Variables (wrangler.jsonc)

{
  "vars": {
    "NEXTAUTH_URL": "https://eventify.today",
    "SITE_URL": "https://eventify.today",
    "NODE_ENV": "production"
  }
}

Local Development (.dev.vars)

NEXTAUTH_SECRET=your-local-secret-min-32-chars
NEXTAUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-secret
BREVO_API_KEY=xkeysib-your-key

Critical Fixes Implemented

Issue #1: D1 Initialization

Problem: Incorrect fallback when D1 binding missing
Solution: Conditional logic for D1 vs direct connection

Issue #2: Missing Adapter in getSession()

Problem: No adapter provided to session retrieval
Solution: Inject adapter from D1 binding

Issue #3: Insecure Cookies

Problem: Missing security attributes
Solution: httpOnly, secure, sameSite, proper naming

Problem: Hardcoded cookie name in JWT logic
Solution: Environment-aware naming

Issue #5: Missing Env Validation

Problem: No validation for NEXTAUTH_SECRET
Solution: Startup validation with actionable error

See full technical review in archived docs for details.

Testing

Unit Tests

  • src/lib/nextauth/custom-email-provider.test.ts (5 tests)
  • Mocks Brevo API
  • Validates provider configuration

Manual Testing Checklist

  • Credentials login with username/password
  • Google OAuth flow
  • Email magic link
  • Session persistence across page loads
  • Sign out clears session
  • Protected routes redirect to login

Local Testing

npx wrangler dev
# Navigate to http://localhost:3000

Preview Testing

npm run preview
# Or deploy preview via PR with "deploy-preview" label

Cloudflare-Specific Considerations

D1 Database Binding

  • Accessed via env.DB from Cloudflare context
  • Configured in wrangler.jsonc under d1_databases
  • Local dev uses .wrangler/state/v3/d1/miniflare-D1Database/<id>.sqlite

Secrets Management

  • Use wrangler secret put for sensitive values
  • Accessible via process.env.* (nodejs_compat flag)
  • Never commit secrets to repository
  • 4KB max size per cookie (session token is ~36 bytes ✅)
  • __Secure- prefix required for HTTPS cookies
  • Enforced by Cloudflare security policies

Cold Start Performance

  • First request after idle may be slower
  • D1 connection initialized on demand
  • Consider edge caching for public routes

Deployment Checklist

  • Run D1 migrations: wrangler d1 migrations apply DB --remote
  • Set all required Cloudflare secrets
  • Verify NEXTAUTH_URL matches domain
  • Test auth flows in staging
  • Monitor Cloudflare logs for errors
  • Confirm session cookies use __Secure- prefix

Troubleshooting

"NEXTAUTH_SECRET is not set"

wrangler secret put NEXTAUTH_SECRET
# Enter a random 32+ character string

Session not persisting

  • Check cookie settings in browser DevTools
  • Verify NEXTAUTH_URL matches current domain
  • Ensure D1 database has Session table

Google OAuth errors

  • Verify redirect URIs in Google Console
  • Check GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET
  • Ensure credentials match environment

D1 connection errors

  • Check wrangler.jsonc D1 binding name matches code
  • Run migrations if tables missing
  • Verify database ID is correct

Additional Resources

  • Cloudflare D1: https://developers.cloudflare.com/d1/
  • OpenNext Cloudflare: https://opennext.js.org/cloudflare
  • NextAuth.js: https://next-auth.js.org/
  • Prisma D1 Adapter: https://www.prisma.io/docs/orm/overview/databases/cloudflare-d1