Skip to content

Latest commit

ย 

History

80 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

better-auth-cloudflare

Seamlessly integrate Better Auth with Cloudflare Workers, D1, Hyperdrive, KV, R2, and geolocation services.

NPM Version NPM Downloads License: MIT

LIVE DEMOS:

Demo implementations are available in the examples/ directory for OpenNextJS โ—† and Hono ๐Ÿ”ฅ, along with recommended scripts for generating database schema, migrating, and more. The library is compatible with any framework that runs on Cloudflare Workers.

Features

  • ๐Ÿ—„๏ธ Database Integration: Support for D1 (SQLite), Postgres, and MySQL databases via Drizzle ORM, or native D1 without Drizzle.
  • ๐Ÿš€ Hyperdrive Support: Connect to Postgres and MySQL databases through Cloudflare Hyperdrive.
  • ๐Ÿ”Œ KV Storage Integration: Optionally use Cloudflare KV for secondary storage (e.g., session caching).
  • ๐Ÿ“ R2 File Storage: Upload, download, and manage user files with Cloudflare R2 object storage and database tracking.
  • ๐Ÿ“ Automatic Geolocation Tracking: Enrich user sessions with location data derived from Cloudflare.
  • ๐ŸŒ Cloudflare IP Detection: Utilize Cloudflare's IP detection headers out-of-the-box.
  • ๐Ÿ” Rich Client-Side Context: Access timezone, city, country, region, and more via the client plugin.
  • ๐Ÿ“ฆ CLI: Tools for getting started quickly with Hono or Next.js, managing database schema, and more.

Roadmap

  • IP Detection
  • Geolocation
  • D1
  • Hyperdrive (Postgres/MySQL)
  • KV
  • R2
  • Cloudflare Email
  • Cloudflare Images
  • Durable Objects
  • D1 Multi-Tenancy

CLI:

  • generate - Create new projects from Hono/Next.js templates with automatic Cloudflare resource setup
  • integrate - Add better-auth-cloudflare to existing projects, creating/updating auth and schema files
  • migrate - Update auth schema and run database migrations when configuration changes
  • plugin - Generate empty Better Auth plugin for quickly adding typesafe endpoints and schema fields
  • version - Check the version of the CLI
  • help - Show all commands and their usage

Examples:

  • Hono
  • OpenNextJS
  • SvelteKit (+ Hyperdrive)
  • TanStack Start (+ Durable Objects)

Table of Contents

Quick Start with CLI

โšก๏ธ For the fastest setup, use the CLI to generate a complete project (including the resources on Cloudflare):

Interactive mode (asks questions and provides helpful defaults):

npx @better-auth-cloudflare/cli@latest generate

Non-interactive mode (use arguments):

# Simple D1 app with KV (fully deployed to Cloudflare)
npx @better-auth-cloudflare/cli@latest generate \
  --app-name=my-auth-app \
  --template=hono \
  --database=d1 \
  --kv=true \
  --r2=false \
  --apply-migrations=prod

Migration workflow:

npx @better-auth-cloudflare/cli@latest migrate                         # Interactive
npx @better-auth-cloudflare/cli@latest migrate --migrate-target=prod   # Non-interactive

The CLI creates projects from Hono or Next.js templates and can automatically set up D1, KV, R2, and Hyperdrive resources. See CLI Documentation for full documentation and all available arguments.

Troubleshooting:

If you encounter this error when using the CLI: ...Error [ERR_REQUIRE_ESM]: require() of ES Module..., make sure your node version is at least v23.0.0, v22.12.0, or v20.19.0, depending on the major version you use. Read more here

Manual Installation

npm install better-auth-cloudflare
# or
yarn add better-auth-cloudflare
# or
pnpm add better-auth-cloudflare
# or
bun add better-auth-cloudflare

Configuration Options

Option Type Default Description
autoDetectIpAddress boolean true Auto-detect IP address from Cloudflare headers
geolocationTracking boolean true Track geolocation data in the session table
cf object {} Cloudflare geolocation context
r2 object undefined R2 bucket configuration for file storage

For the full WithCloudflareOptions interface (including database, KV, and Drizzle adapter options), see the Configuration Reference.

Setup

Integrating better-auth-cloudflare into your project involves a few key steps to configure your database, authentication logic, and API routes. Follow these instructions to get started:


1. Define Your Database Schema (src/db/schema.ts)

You'll need to merge the Better Auth schema with any other Drizzle schemas your application uses. This ensures that Drizzle can manage your entire database structure, including the tables required by Better Auth.

import * as authSchema from "./auth.schema"; // This will be generated in a later step

// Combine all schemas here for migrations
export const schema = {
    ...authSchema,
    // ... your other application schemas
} as const;

Note: The auth.schema.ts file will be generated by the Better Auth CLI in a subsequent step.


2. Initialize Drizzle ORM (src/db/index.ts)

Properly initialize Drizzle with your database. This function will provide a database client instance to your application. For D1, you'll use Cloudflare D1 bindings, while Postgres/MySQL will use Hyperdrive connection strings.

import { getCloudflareContext } from "@opennextjs/cloudflare";
import { drizzle } from "drizzle-orm/d1";
import { schema } from "./schema";

export async function getDb() {
    // Retrieves Cloudflare-specific context, including environment variables and bindings
    const { env } = await getCloudflareContext({ async: true });

    // Initialize Drizzle with your D1 binding (e.g., "DB" or "DATABASE" from wrangler.toml)
    return drizzle(env.DATABASE, {
        // Ensure "DATABASE" matches your D1 binding name in wrangler.toml
        schema,
        logger: true, // Optional
    });
}

3. Configure Better Auth (src/auth/index.ts)

Set up your Better Auth configuration, wrapping it with withCloudflare to enable Cloudflare-specific features. The exact configuration depends on your framework:

For most frameworks (Hono, etc.):

import type { D1Database, IncomingRequestCfProperties } from "@cloudflare/workers-types";
import { betterAuth } from "better-auth";
import { withCloudflare } from "better-auth-cloudflare";
import { drizzleAdapter } from "@better-auth/drizzle-adapter";
import { drizzle } from "drizzle-orm/d1";
import { schema } from "../db";

// Single auth configuration that handles both CLI and runtime scenarios
function createAuth(env?: CloudflareBindings, cf?: IncomingRequestCfProperties, baseURL?: string) {
    // Use actual DB for runtime, empty object for CLI
    const db = env ? drizzle(env.DATABASE, { schema, logger: true }) : ({} as any);

    return betterAuth({
        baseURL,
        ...withCloudflare(
            {
                autoDetectIpAddress: true,
                geolocationTracking: true,
                cf: cf || {},
                d1: env
                    ? {
                          db,
                          options: {
                              usePlural: true,
                              debugLogs: true,
                          },
                      }
                    : undefined,
                kv: env?.KV,
                kvAtomicCompatibility: env?.KV ? true : undefined,
                // Optional: Enable R2 file storage
                r2: env?.R2_BUCKET
                    ? {
                          bucket: env.R2_BUCKET,
                          maxFileSize: 10 * 1024 * 1024, // 10MB
                          allowedTypes: [".jpg", ".jpeg", ".png", ".gif", ".pdf", ".doc", ".docx"],
                          additionalFields: {
                              category: { type: "string", required: false },
                              isPublic: { type: "boolean", required: false },
                              description: { type: "string", required: false },
                          },
                      }
                    : undefined,
            },
            {
                emailAndPassword: {
                    enabled: true,
                },
                verification: {
                    storeInDatabase: true,
                },
                rateLimit: {
                    enabled: true,
                    storage: "database",
                },
            }
        ),
        // Only add database adapter for CLI schema generation
        ...(env
            ? {}
            : {
                  database: drizzleAdapter({} as D1Database, {
                      provider: "sqlite",
                      usePlural: true,
                      debugLogs: true,
                  }),
              }),
    });
}

// Export for CLI schema generation
export const auth = createAuth();

// Export for runtime usage
export { createAuth };

The baseURL is derived per-request in Hono middleware via new URL(c.req.url).origin. On Cloudflare Workers, request.url reflects the actual URL the client connected to โ€” Cloudflare's edge routes requests to your worker based on DNS and route configuration, not the HTTP Host header alone. Alternatively, you can set the BETTER_AUTH_URL environment variable and omit the baseURL parameter.

For OpenNext.js with complex async requirements: See the OpenNext.js example for a more complex configuration that handles async database initialization and singleton patterns.

Using Hyperdrive (MySQL):

import { drizzle } from "drizzle-orm/mysql2";
import mysql from "mysql2/promise";

async function getDb() {
    const { env } = await getCloudflareContext({ async: true });
    const connection = mysql.createPool(env.HYPERDRIVE_URL);
    return drizzle(connection, { schema });
}

const auth = betterAuth({
    ...withCloudflare(
        {
            mysql: {
                db: await getDb(),
            },
            // other cloudflare options...
        },
        {
            // your auth options...
        }
    ),
});

Using Hyperdrive (Postgres):

import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";

async function getDb() {
    const { env } = await getCloudflareContext({ async: true });
    const sql = postgres(env.HYPERDRIVE_URL);
    return drizzle(sql, { schema });
}

const auth = betterAuth({
    ...withCloudflare(
        {
            postgres: {
                db: await getDb(),
            },
            // other cloudflare options...
        },
        {
            // your auth options...
        }
    ),
});

Using Native D1 (no Drizzle required):

If you don't need Drizzle ORM's type-safe schema and migration tooling, you can pass a D1 binding directly. better-auth uses its built-in Kysely D1 dialect under the hood:

import { betterAuth } from "better-auth";
import { withCloudflare } from "better-auth-cloudflare";

const auth = betterAuth({
    ...withCloudflare(
        {
            d1Native: env.DATABASE, // D1Database binding from wrangler.toml
            kv: env.KV,
            // other cloudflare options...
        },
        {
            // your auth options...
        }
    ),
});

This path does not require @better-auth/drizzle-adapter at all. Trade-offs vs. the Drizzle D1 option:

d1Native d1 (Drizzle)
Bundle size Smaller Larger (includes Drizzle)
Schema management Manual SQL / better-auth CLI Drizzle Kit migrations
Type-safe queries No Yes
Setup complexity Simpler More boilerplate

Using better-auth/minimal for smaller bundles:

better-auth v1.5+ provides a better-auth/minimal entry point that tree-shakes unused features for smaller Worker bundles:

import { betterAuth } from "better-auth/minimal";
import { withCloudflare } from "better-auth-cloudflare";

This works as a drop-in replacement for betterAuth from "better-auth" but excludes features you haven't explicitly imported (e.g., social providers, admin endpoints).

4. Generate and Manage Auth Schema

Better Auth uses Drizzle ORM for database interactions, allowing for automatic schema management for your database (D1/SQLite, Postgres, or MySQL).

To generate or update your authentication-related database schema, run the Better Auth CLI:

npx @better-auth/cli@latest generate

This command inspects your src/auth/index.ts (specifically the auth export) and creates/updates src/db/auth.schema.ts with the necessary Drizzle schema definitions for tables like users, sessions, accounts, etc.

Recommended Usage:

Specify your configuration file and output path for more precise control:

npx @better-auth/cli@latest generate --config src/auth/index.ts --output src/db/auth.schema.ts -y

This command will:

  • Read the export const auth configuration from src/auth/index.ts.
  • Output the generated Drizzle schema to src/db/auth.schema.ts.
  • Automatically confirm prompts (-y).

After generation, you can use Drizzle Kit to create and apply migrations to your database. Refer to the Drizzle ORM documentation for managing migrations.

For integrating the generated auth.schema.ts with your existing Drizzle schema, see managing schema across multiple files. More details on schema generation are available in the Better Auth docs.

5. Configure KV as Secondary Storage (Optional)

If you provide a KV namespace in the withCloudflare configuration (as shown in src/auth/index.ts), it will be used as Secondary Storage by Better Auth. This is typically used for caching or storing session data that doesn't need to reside in your primary database.

Ensure your KV namespace (e.g., USER_SESSIONS) is correctly bound in your wrangler.toml file.

Important: KV and session revocation

Better Auth resolves a session by checking secondary storage before the database, and returns immediately on a hit without consulting the database:

// better-auth: internal-adapter, findSession
if (secondaryStorage) {
    const sessionStringified = await secondaryStorage.get(token);
    if (sessionStringified) {
        // returns here; the database is never consulted
    }
}

Workers KV is eventually consistent. Changes may take 60 seconds or more to appear in another location. A stale positive session hit therefore bypasses the mirrored database after logout or another direct token revocation.

Better Auth also maintains each user's active-session list with separate secondary-storage reads and writes. Concurrent session changes can lose a token reference, so bulk revocation, role or ban changes, or user deletion may miss that cached token until its original session expiry. A strongly consistent secondary store removes KV propagation lag for direct token reads and deletes, but it does not make the active-session update atomic.

session.storeSessionInDatabase: true does not repair a stale positive hit. If bulk revocation, role or ban changes, or user deletion must take effect immediately everywhere, omit secondary session caching and leave session.cookieCache disabled unless Better Auth adds an atomic active-list update.

Better Auth 1.7 atomic storage requirements

Better Auth 1.7 requires secondary storage to atomically consume verification values and increment rate-limit counters. Workers KV cannot provide either operation. When using KV with Better Auth 1.7, route those operations explicitly:

verification: {
    storeInDatabase: true,
},
rateLimit: {
    storage: "database",
},

Also set kvAtomicCompatibility: true next to the kv binding. It validates this configuration at startup and does not select a storage backend for you.

Database-backed rate limiting typically adds at least one database read and one write to accepted Better Auth requests. Contention, resets, cleanup, and rejected requests can add operations. This affects latency and billing, so the library does not enable it automatically. rateLimit.customStorage can provide an atomic consume implementation backed by a strongly consistent store such as Redis or Durable Objects. storage: "memory" avoids database traffic but is not a distributed rate limit on Workers.

createKVStorage() deliberately exposes only KV's non-atomic get, set, and delete operations. Use withCloudflare() for Better Auth 1.7 so the package can wire KV session storage while the settings above keep atomic operations elsewhere.

Database-backed rate limiting requires Better Auth's rate-limit table. Generate the 1.7 schema with the same auth package version you deploy. For a populated 1.6 database, do not apply a plain generated schema. Follow the Better Auth 1.7 migration guide, including auth migrate plan and a rehearsed migration against a restored backup.

Important: KV TTL Limitation

Workers KV has a minimum physical TTL of 60 seconds. createKVStorage() clamps shorter TTLs to 60 seconds and logs a warning. Better Auth 1.5 and 1.6 rate limiting keeps its own timestamps, so a shorter logical window can still expire while the KV key remains stored. Do not weaken Better Auth's protected sign-in rules just to match KV's physical TTL.

6. Set Up API Routes

Create API routes to handle authentication requests. Better Auth provides a handler that can be used for various HTTP methods.

// Example: src/app/api/auth/[...all]/route.ts
// Adjust the path based on your project structure (e.g., Next.js App Router)

import { initAuth } from "@/auth"; // Adjust path to your auth/index.ts

export async function POST(req: Request) {
    const auth = await initAuth();
    return auth.handler(req);
}

export async function GET(req: Request) {
    const auth = await initAuth();
    return auth.handler(req);
}

// You can also add handlers for PUT, DELETE, PATCH if needed by your auth flows

7. Initialize the Client

Set up the Better Auth client, including the Cloudflare plugin, to interact with authentication features on the front-end.

// Example: src/lib/authClient.ts or similar client-side setup file

import { createAuthClient } from "better-auth/client";
import { cloudflareClient } from "better-auth-cloudflare/client";

const authClient = createAuthClient({
    plugins: [cloudflareClient()], // includes geolocation and R2 file features (if configured)
});

export default authClient;

Usage Examples

Accessing Geolocation Data

This library enables access to Cloudflare's geolocation data both on the client and server-side.

Client-side API: Use the authClient to fetch geolocation information.

import authClient from "@/lib/authClient"; // Adjust path to your client setup

const displayLocationInfo = async () => {
    try {
        const result = await authClient.cloudflare.geolocation();
        if (result.error) {
            console.error("Error fetching geolocation:", result.error);
        } else if (result.data && !("error" in result.data)) {
            console.log("๐Ÿ“ Geolocation data:", {
                timezone: result.data.timezone,
                city: result.data.city,
                country: result.data.country,
                region: result.data.region,
                regionCode: result.data.regionCode,
                colo: result.data.colo,
                latitude: result.data.latitude,
                longitude: result.data.longitude,
            });
        }
    } catch (err) {
        console.error("Failed to get geolocation data:", err);
    }
};

displayLocationInfo();

R2 File Storage

If you've configured R2 in your server setup, you can upload and manage files:

import authClient from "@/lib/authClient";

// Upload a file with metadata
const uploadFile = async (file: File) => {
    const result = await authClient.uploadFile(file, {
        category: "documents",
        isPublic: false,
        description: "Important document",
    });

    if (result.error) {
        console.error("Upload failed:", result.error.message || "Failed to upload file. Please try again.");
    } else {
        console.log("File uploaded:", result.data);
    }
};

// List user's files
const listFiles = async () => {
    const result = await authClient.files.list();
    if (result.data) {
        console.log("User files:", result.data);
    }
};

// Download a file
const downloadFile = async (fileId: string, filename: string) => {
    const result = await authClient.files.download({ fileId });
    if (result.error) {
        console.error("Download failed:", result.error);
        return;
    }

    // Extract blob and create download
    const response = result.data;
    const blob = response instanceof Response ? await response.blob() : response;
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = filename;
    a.click();
    window.URL.revokeObjectURL(url);
};

For complete R2 file storage documentation, see the R2 File Storage Guide.

License

MIT

Contributing

Contributions are welcome! Whether it's bug fixes, feature additions, or documentation improvements, we appreciate your help in making this project better. For major changes or new features, please open an issue first to discuss what you would like to change.

About

Seamlessly integrate better-auth with Cloudflare Workers, D1, Hyperdrive, KV, R2, and geolocation services. CLI for project generation, automated resource provisioning on Cloudflare, and database migrations. Supports Next.js, Hono, and more!

Topics

Resources

Stars

585 stars

Watchers

7 watching

Forks

Releases

Used by

Contributors

Languages