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
3. Email Magic Link (Brevo)¶
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¶
Cookie Configuration¶
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
Issue #4: Inconsistent Cookie Names¶
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¶
Preview Testing¶
Cloudflare-Specific Considerations¶
D1 Database Binding¶
- Accessed via
env.DBfrom Cloudflare context - Configured in
wrangler.jsoncunderd1_databases - Local dev uses
.wrangler/state/v3/d1/miniflare-D1Database/<id>.sqlite
Secrets Management¶
- Use
wrangler secret putfor sensitive values - Accessible via
process.env.*(nodejs_compat flag) - Never commit secrets to repository
Cookie Limitations¶
- 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_URLmatches domain - Test auth flows in staging
- Monitor Cloudflare logs for errors
- Confirm session cookies use
__Secure-prefix
Troubleshooting¶
"NEXTAUTH_SECRET is not set"¶
Session not persisting¶
- Check cookie settings in browser DevTools
- Verify
NEXTAUTH_URLmatches current domain - Ensure D1 database has Session table
Google OAuth errors¶
- Verify redirect URIs in Google Console
- Check
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET - Ensure credentials match environment
D1 connection errors¶
- Check
wrangler.jsoncD1 binding name matches code - Run migrations if tables missing
- Verify database ID is correct
Related Documentation¶
- Email System - Brevo integration for magic links
- Legacy Systems - D1 data handling and migrations
- Deployment - Cloudflare deployment guide
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