# How to Implement Authentication with Better Auth on Cloudflare Workers

> Implement authentication on Cloudflare Workers using Better Auth, Drizzle, and Next.js. Learn to lazily initialize Better Auth and manage sessions with React for a secure application.

- Repository: [Muhammad Arifin/fullstack-next-cloudflare](https://github.com/ifindev/fullstack-next-cloudflare)
- Tags: how-to-guide
- Published: 2026-03-03

---

**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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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.

```typescript
// 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`.

```typescript
// 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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/auth/utils/auth-client.ts) initializes this client using `createAuthClient` from `better-auth/react`.

```typescript
// 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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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.

```tsx
// 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.

```typescript
// 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:

1. **Browser** sends credentials to `/api/auth/sign-in` (handled by the catch-all route).
2. **Route Handler** calls `createHandler()`, which retrieves the cached Better Auth instance via `getAuthInstance()`.
3. **Better Auth Core** validates credentials against the SQLite database via the Drizzle adapter.
4. **Session Creation** generates a token and sets an HTTP-only cookie using the `nextCookies()` plugin.
5. **Subsequent Requests** automatically include the session cookie; calling `getCurrentUser()` or `requireAuth()` 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 `cachedAuth` singleton in [`src/modules/auth/utils/auth-utils.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/auth/utils/auth-utils.ts) to minimize cold start overhead in the serverless environment.
- **Bridge to Next.js** by wrapping `auth.handler` with `toNextJsHandler` in the catch-all route at `src/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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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 `createAuthClient` from `better-auth/react`, exposing methods like `signIn` and `useSession`.
- **Protect server logic** with `requireAuth()` and `isAuthenticated()` 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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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.