How to Implement Authentication with Better Auth on Cloudflare Workers
You can implement authentication on Cloudflare Workers by lazily initializing a singleton Better Auth instance with a Drizzle SQLite adapter, exposing it through Next.js catch-all API routes using toNextJsHandler, and consuming sessions via a React client created with createAuthClient.
The ifindev/fullstack-next-cloudflare repository demonstrates a complete, edge-compatible authentication stack for Next.js applications running on Cloudflare Workers. This architecture eliminates external auth services by leveraging Better Auth directly within the Worker runtime, using SQLite for persistence while maintaining full type safety through Drizzle ORM.
Core Architecture Components
Lazy-Initialized Auth Singleton
The implementation in src/modules/auth/utils/auth-utils.ts avoids cold-start penalties by caching the Better Auth instance in a module-level variable. This pattern ensures that expensive initialization—database adapter setup, provider configuration, and plugin registration—runs only once per Worker invocation.
// src/modules/auth/utils/auth-utils.ts
import { getCloudflareContext } from "@opennextjs/cloudflare";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { nextCookies } from "better-auth/next-js";
import { getDb } from "@/db";
let cachedAuth: ReturnType<typeof betterAuth> | null = null;
export async function getAuthInstance() {
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!,
},
},
plugins: [nextCookies()],
});
return cachedAuth;
}
The getAuthInstance function reads environment variables from the Cloudflare context via getCloudflareContext(), ensuring credentials remain secure and are not bundled in client-side code. The drizzleAdapter connects to the SQLite database returned by getDb(), while the nextCookies() plugin handles HTTP-only cookie serialization required for session management in Next.js on Workers.
Server-Side API Route Bridge
Better Auth exposes a universal handler that must be adapted to Next.js's route signature. The file src/app/api/auth/[...all]/route.ts creates this bridge using toNextJsHandler from better-auth/next-js.
// 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 catch-all route captures all authentication-related requests—sign-in, sign-up, callback handling, and session validation—forwarding them to the Better Auth core running inside the Worker.
Client-Side React Integration
Components interact with the authentication API through a type-safe client. The file src/modules/auth/utils/auth-client.ts initializes this client using createAuthClient from better-auth/react.
// src/modules/auth/utils/auth-client.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({});
This exported authClient provides methods like signIn(), signUp(), signOut(), and useSession() that communicate with the server-side handlers established in the previous step.
Server-Side Authentication Helpers
To simplify protected logic, src/modules/auth/utils/auth-utils.ts exports higher-level utilities that wrap session validation:
getCurrentUser()– Resolves the full user object from the current session.requireAuth()– Validates the session and throws an error if the user is unauthenticated, ideal for gating server actions.isAuthenticated()– Returns a boolean indicating session presence without throwing.
Implementation Walkthrough
Step 1: Configure the Better Auth Instance
Create src/modules/auth/utils/auth-utils.ts to manage the singleton pattern. This module must dynamically import Cloudflare-specific modules to ensure they only execute in the server context. The configuration binds Better Auth to your SQLite database and registers the social providers you intend to support.
Step 2: Create the Catch-All API Route
Establish the HTTP interface at src/app/api/auth/[...all]/route.ts. This file must export GET and POST handlers that lazy-load the auth instance and adapt it to Next.js expectations. Placing the route at [...all] ensures Better Auth receives all sub-paths like /api/auth/sign-in or /api/auth/callback/google.
Step 3: Consume Auth in React Components
Import the client factory in your UI components to trigger authentication flows. The authClient methods automatically handle CSRF protection and cookie attachment required by the edge runtime.
// Example: Sign-up component
import { authClient } from "@/modules/auth/utils/auth-client";
export default function SignUp() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.currentTarget as HTMLFormElement);
await authClient.signUp({
email: formData.get("email") as string,
password: formData.get("password") as string,
});
};
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Create Account</button>
</form>
);
}
Step 4: Protect Server Actions and API Routes
Use the requireAuth helper to enforce authentication in server-side logic. This function reads the incoming request cookies, validates the session against the SQLite store via Better Auth's internal API, and returns the user object or throws.
// Example: Protected server action
import { requireAuth } from "@/modules/auth/utils/auth-utils";
export async function deleteUserAccount() {
const user = await requireAuth(); // Throws 401 if not logged in
// Execute privileged logic
await db.delete(users).where(eq(users.id, user.id));
return { success: true };
}
Request Lifecycle and Data Flow
When a user initiates sign-in, the data flows through the stack as follows:
- Browser sends credentials to
/api/auth/sign-in(handled by the catch-all route). - Route Handler calls
createHandler(), which retrieves the cached Better Auth instance viagetAuthInstance(). - Better Auth Core validates credentials against the SQLite database via the Drizzle adapter.
- Session Creation generates a token and sets an HTTP-only cookie using the
nextCookies()plugin. - Subsequent Requests automatically include the session cookie; calling
getCurrentUser()orrequireAuth()in server code resolves the user by querying Better Auth's session store.
This architecture keeps all authentication logic—database queries, session encryption, and OAuth handling—within the Cloudflare Workers runtime, eliminating network latency to external auth services.
Summary
- Lazy-load the Better Auth instance using a
cachedAuthsingleton insrc/modules/auth/utils/auth-utils.tsto minimize cold start overhead in the serverless environment. - Bridge to Next.js by wrapping
auth.handlerwithtoNextJsHandlerin the catch-all route atsrc/app/api/auth/[...all]/route.ts. - Persist data locally via the Drizzle SQLite adapter, storing user credentials and sessions in the Cloudflare-compatible SQLite database configured in
src/db/index.ts. - Manage cookies using the
nextCookies()plugin, which is specifically designed for Next.js applications running on Cloudflare Workers. - Consume on the client through
createAuthClientfrombetter-auth/react, exposing methods likesignInanduseSession. - Protect server logic with
requireAuth()andisAuthenticated()helpers that validate sessions against the Better Auth core before executing privileged code.
Frequently Asked Questions
How does Better Auth store session data on Cloudflare Workers?
Better Auth stores session data in the SQLite database provided by Cloudflare's infrastructure (either D1 or local SQLite bindings). The drizzleAdapter configured with provider: "sqlite" writes session tokens and user metadata to this database, while the nextCookies() plugin serializes session identifiers into HTTP-only cookies that travel with each request.
Why is a singleton pattern required for the Better Auth instance?
The cachedAuth variable in src/modules/auth/utils/auth-utils.ts prevents the expensive re-initialization of database connections, adapter setup, and provider configuration on every incoming request. In Cloudflare Workers' serverless environment, this caching ensures that warm invocations reuse the existing instance, significantly reducing latency and maintaining efficient connection pooling with the SQLite database.
How do I access environment variables in the Better Auth configuration?
Environment variables are accessed via getCloudflareContext() from the @opennextjs/cloudflare package. This function retrieves the env object containing secrets like BETTER_AUTH_SECRET and OAuth credentials. Unlike standard Next.js applications that might access process.env, Cloudflare Workers require this async context retrieval to access bound secrets and variables securely.
Can I use social authentication providers with this setup?
Yes. The configuration in src/modules/auth/utils/auth-utils.ts supports social providers by reading GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET from the Cloudflare environment and passing them to the socialProviders.google configuration object. Better Auth handles the OAuth flow internally, including callback routing through the catch-all API handler, session creation, and user record insertion via the Drizzle adapter.
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 →