How to Handle Authentication Session Cookies Between Next.js and Cloudflare Workers
The ifindev/fullstack-next-cloudflare repository uses the better-auth library with the nextCookies plugin to store sessions in HTTP-only cookies, making the same authentication token automatically available to both Next.js runtime handlers and Cloudflare Worker edge middleware without custom header forwarding.
When running Next.js on Cloudflare Workers, the edge runtime executes your application code before requests reach the page router. This creates a challenge: how do you validate user sessions in the middleware layer while maintaining access to that same session inside your API routes and Server Actions? The solution lies in leveraging standard HTTP cookie semantics that work identically across both execution contexts.
The Architecture Overview
The architecture centers on better-auth configured with the nextCookies plugin. This combination stores the user session in a standard HTTP-only cookie that browsers automatically attach to every request. Because Cloudflare Workers execute the Next.js middleware before the request hits your application code, and because both environments read from the same Cookie header, you get seamless session sharing without duplicated token stores or complex synchronization logic.
How the nextCookies Plugin Enables Cross-Runtime Access
In src/modules/auth/utils/auth-utils.ts, the auth instance is configured with the nextCookies() plugin:
// src/modules/auth/utils/auth-utils.ts
import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next-js";
let cachedAuth: ReturnType<typeof betterAuth> | null = null;
async function getAuth() {
if (cachedAuth) return cachedAuth;
const { env } = await getCloudflareContext();
const db = await getDb();
cachedAuth = betterAuth({
secret: env.BETTER_AUTH_SECRET,
database: drizzleAdapter(db, { provider: "sqlite" }),
emailAndPassword: { enabled: true },
socialProviders: {
google: {
enabled: true,
clientId: env.GOOGLE_CLIENT_ID!,
clientSecret: env.GOOGLE_CLIENT_SECRET!,
},
},
// ← This plugin makes the session cookie work on both Next.js and Workers
plugins: [nextCookies()],
});
return cachedAuth;
}
The nextCookies plugin handles the underlying cookie serialization and deserialization, ensuring that when auth.api.getSession({ headers }) is called in any context—whether inside a React Server Component, a Server Action, or the edge middleware—it reads from the same cookie store.
The Five-Step Authentication Flow
-
Login initiation: The client calls
/api/auth/...routes generated bybetter-auth/next-js. The server creates a cryptographically signed session cookie and returns it viaSet-Cookieheaders. -
Automatic propagation: The browser stores the HTTP-only cookie and attaches it to every subsequent request, regardless of whether the target is a Next.js page, a Server Action, or the Worker middleware.
-
Server-side validation: Application code uses helpers from
src/modules/auth/utils/auth-utils.tsto retrieve the current user:getCurrentUser()returns the user payload ornullby callingauth.api.getSession({ headers }).requireAuth()throws an error if no valid session exists, ideal for protected data fetching.isAuthenticated()returns a boolean flag for conditional rendering logic.
-
Edge protection:
src/middleware.tsruns before the request reaches the page router. It initializes the same auth singleton viagetAuthInstance()and validates the session usingauth.api.getSession({ headers: request.headers }). Missing sessions trigger an immediate redirect to/login. -
Client-side consistency: The browser-side client in
src/modules/auth/utils/auth-client.tspoints to the same/api/auth/...endpoints. OAuth flows and password sign-ins automatically set the cookie that the Worker later validates.
Implementing the Shared Session Cookie
Creating the Central Auth Instance
The singleton pattern in src/modules/auth/utils/auth-utils.ts ensures that both the middleware and your application routes use the same configuration and database connection. This file exports getAuthInstance() (aliased as getAuth in some contexts) which caches the auth object between requests in the same Worker instance.
Protecting Routes at the Edge
The src/middleware.ts file demonstrates how to intercept requests before they consume application resources:
// src/middleware.ts
import { getAuthInstance as getAuth } from "@/modules/auth/utils/auth-utils";
import { NextResponse } from "next/server";
export async function middleware(request: Request) {
const auth = await getAuth();
const session = await auth.api.getSession({ headers: request.headers });
if (!session) {
// No cookie → redirect to login page before the request reaches Next.js
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
This middleware efficiently validates the session cookie at the network edge, preventing unauthenticated requests from ever hitting your database or rendering logic.
Accessing Sessions in Server Components and Actions
For protected data fetching, use the requireAuth() helper inside Server Actions:
// Example: src/modules/todos/actions/get-todos.action.ts
import { requireAuth } from "@/modules/auth/utils/auth-utils";
export async function getTodos() {
const user = await requireAuth(); // throws if no valid cookie
// `user` now contains { id, name, email }
// … fetch data for this user …
}
This pattern ensures that any attempt to access protected resources without a valid session immediately fails with an authentication error.
Setting Up the Dynamic API Handler
The auth routes must be exposed through a catch-all API handler in src/app/api/auth/[...all]/route.ts:
// src/app/api/auth/[...all]/route.ts
import { toNextJsHandler } from "better-auth/next-js";
import { getAuthInstance } from "@/modules/auth/utils/auth-utils";
const createHandler = async () => {
const auth = await getAuthInstance();
return toNextJsHandler(auth.handler);
};
export async function GET(request: Request) {
const { GET: handler } = await createHandler();
return handler(request);
}
export async function POST(request: Request) {
const { POST: handler } = await createHandler();
return handler(request);
}
This dynamic route forwards all authentication-related requests (login, logout, callbacks) to the better-auth handler, which manages the session cookie lifecycle.
Client-Side Integration
On the browser, initiate sign-in using the client from src/modules/auth/utils/auth-client.ts:
// src/modules/auth/components/login-form.tsx
import { authClient } from "@/modules/auth/utils/auth-client";
const signInWithGoogle = async () => {
await authClient.signIn.social({
provider: "google",
// After a successful OAuth flow, the server sets the session cookie
callbackURL: "/dashboard",
});
};
Once the OAuth flow completes, the server sets the HTTP-only cookie, and subsequent navigation to /dashboard passes through the middleware validation successfully.
Summary
- Use the
nextCookiesplugin from better-auth to store sessions in standard HTTP-only cookies readable by both Next.js and Cloudflare Workers. - Implement a singleton auth instance in
src/modules/auth/utils/auth-utils.tsto share configuration between middleware and application code. - Validate at the edge in
src/middleware.tsusingauth.api.getSession({ headers: request.headers })to protect routes before they reach your application. - Leverage helper functions like
requireAuth()andgetCurrentUser()for type-safe session access inside Server Components and Actions. - Maintain consistency by using the same auth client on the browser and the same API routes for all authentication flows.
Frequently Asked Questions
How does the nextCookies plugin enable session sharing between Next.js and Workers?
The nextCookies plugin serializes the user session into a standard HTTP-only cookie that conforms to browser specifications. Because Cloudflare Workers execute the Next.js middleware in the same request context as the application runtime, both environments read from the identical Cookie header. This eliminates the need for separate token stores or custom header injection between the edge and the application server.
Can I use this authentication pattern with other providers besides Google?
Yes. The configuration in src/modules/auth/utils/auth-utils.ts supports multiple social providers through the socialProviders object, and the emailAndPassword option enables credential-based authentication. The session cookie mechanism works identically regardless of the authentication method used, as the nextCookies plugin handles cookie operations independently of the provider strategy.
How does the middleware protect routes before they reach the Next.js router?
The src/middleware.ts file executes at the Cloudflare Worker edge before the request enters the Next.js routing layer. By calling auth.api.getSession({ headers: request.headers }), it inspects the incoming cookie and returns a NextResponse.redirect() to /login if validation fails. This prevents unauthenticated requests from consuming compute resources or accessing database connections in your application code.
Is the session cookie secure for production deployments?
The implementation uses HTTP-only cookies, which prevent JavaScript access on the client side, reducing XSS attack vectors. The cookies are cryptographically signed using the BETTER_AUTH_SECRET environment variable. For production, ensure you configure the secure attribute (automatically handled by better-auth in HTTPS environments) and set appropriate SameSite policies based on your domain architecture.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →