Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 66 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# better-auth-cloudflare

Seamlessly integrate [Better Auth](https://github.com/better-auth/better-auth) with Cloudflare Workers, D1, KV, and geolocation services.
Seamlessly integrate [Better Auth](https://github.com/better-auth/better-auth) with Cloudflare Workers, D1, Hyperdrive, KV, R2, and geolocation services.

[![NPM Version](https://img.shields.io/npm/v/better-auth-cloudflare)](https://www.npmjs.com/package/better-auth-cloudflare)
[![NPM Downloads](https://img.shields.io/npm/dt/better-auth-cloudflare)](https://www.npmjs.com/package/better-auth-cloudflare)
Expand All @@ -15,7 +15,8 @@ Demo implementations are available in the [`examples/`](./examples/) directory f

## Features

- 🗄️ **D1 Database Integration**: Leverage Cloudflare D1 as your primary database via Drizzle ORM.
- 🗄️ **Database Integration**: Support for D1 (SQLite), Postgres, and MySQL databases via Drizzle ORM.
- 🚀 **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.
Expand All @@ -27,6 +28,7 @@ Demo implementations are available in the [`examples/`](./examples/) directory f
- [x] IP Detection
- [x] Geolocation
- [x] D1
- [x] Hyperdrive (Postgres/MySQL)
- [x] KV
- [x] R2
- [ ] Cloudflare Images
Expand All @@ -36,8 +38,8 @@ Demo implementations are available in the [`examples/`](./examples/) directory f

- [x] Hono
- [x] OpenNextJS
- [ ] SvelteKit
- [ ] TanStack Start
- [ ] SvelteKit (+ Hyperdrive)
- [ ] TanStack Start (+ Durable Objects)

## Table of Contents

Expand All @@ -47,7 +49,7 @@ Demo implementations are available in the [`examples/`](./examples/) directory f
- [1. Define Your Database Schema (`src/db/schema.ts`)](#1-define-your-database-schema-srcdbschemats)
- [2. Initialize Drizzle ORM (`src/db/index.ts`)](#2-initialize-drizzle-orm-srcdbindexts)
- [3. Configure Better Auth (`src/auth/index.ts`)](#3-configure-better-auth-srcauthindexts)
- [4. Generate and Manage Auth Schema with D1](#4-generate-and-manage-auth-schema-with-d1)
- [4. Generate and Manage Auth Schema](#4-generate-and-manage-auth-schema)
- [5. Configure KV as Secondary Storage (Optional)](#5-configure-kv-as-secondary-storage-optional)
- [6. Set Up API Routes](#6-set-up-api-routes)
- [7. Initialize the Client](#7-initialize-the-client)
Expand Down Expand Up @@ -104,7 +106,7 @@ _Note: The `auth.schema.ts` file will be generated by the Better Auth CLI in a s

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

Properly initialize Drizzle with your Cloudflare D1 binding. This function will provide a database client instance to your application, configured to use your D1 database.
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.

```typescript
import { getCloudflareContext } from "@opennextjs/cloudflare";
Expand Down Expand Up @@ -205,9 +207,63 @@ export { createAuth };
**For OpenNext.js with complex async requirements:**
See the [OpenNext.js example](./examples/opennextjs/README.md) for a more complex configuration that handles async database initialization and singleton patterns.

### 4. Generate and Manage Auth Schema with D1
**Using Hyperdrive (MySQL):**

Better Auth uses Drizzle ORM for database interactions, allowing for automatic schema management for your D1 database.
```typescript
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):**

```typescript
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...
}
),
});
```

### 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:

Expand All @@ -231,13 +287,13 @@ This command will:
- 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 D1 database. Refer to the [Drizzle ORM documentation](https://orm.drizzle.team/kit/overview) for managing migrations.
After generation, you can use Drizzle Kit to create and apply migrations to your database. Refer to the [Drizzle ORM documentation](https://orm.drizzle.team/kit/overview) for managing migrations.

For integrating the generated `auth.schema.ts` with your existing Drizzle schema, see [managing schema across multiple files](https://orm.drizzle.team/docs/sql-schema-declaration#schema-in-multiple-files). More details on schema generation are available in the [Better Auth docs](https://www.better-auth.com/docs/adapters/drizzle#schema-generation--migration).

### 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](https://www.better-auth.com/docs/concepts/database#secondary-storage) by Better Auth. This is typically used for caching or storing session data that doesn't need to reside in your primary D1 database.
If you provide a KV namespace in the `withCloudflare` configuration (as shown in `src/auth/index.ts`), it will be used as [Secondary Storage](https://www.better-auth.com/docs/concepts/database#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.

Expand Down
34 changes: 28 additions & 6 deletions src/index.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/zpg6/better-auth-cloudflare/pull/9/files#diff-a2a171449d862fe29692ce031981047d7ab755ae7f84c707aef80701b3ea0c80R5-R11
Could you change it to add .js at the end, like I did?

I'm not sure if changing the tsconfig settings will solve the issue, but for now, Vite environments are encountering module resolution errors when importing like below:

  Error [ERR_MODULE_NOT_FOUND]: Cannot find module './schema' imported from 
  './dist/index.js'

I don't think these changes affect the other examples.

If this is applied, I think I’ll be able to update just the example in the SvelteKit example PR after rebasing, without change anything else. I noticed that a type error occurs, but works well.

Original file line number Diff line number Diff line change
Expand Up @@ -186,14 +186,36 @@ export const withCloudflare = <T extends BetterAuthOptions>(
// If user explicitly set it to true/false, that will be respected.
}

// Assert that only one database configuration is provided
const dbConfigs = [cloudFlareOptions.postgres, cloudFlareOptions.mysql, cloudFlareOptions.d1].filter(Boolean);
if (dbConfigs.length > 1) {
throw new Error(
"Only one database configuration can be provided. Please provide only one of postgres, mysql, or d1."
);
}

// Determine which database configuration to use
let database;
if (cloudFlareOptions.postgres) {
database = drizzleAdapter(cloudFlareOptions.postgres.db, {
provider: "pg",
...cloudFlareOptions.postgres.options,
});
} else if (cloudFlareOptions.mysql) {
database = drizzleAdapter(cloudFlareOptions.mysql.db, {
provider: "mysql",
...cloudFlareOptions.mysql.options,
});
} else if (cloudFlareOptions.d1) {
database = drizzleAdapter(cloudFlareOptions.d1.db, {
provider: "sqlite",
...cloudFlareOptions.d1.options,
});
}

Comment on lines +189 to +215
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about enforce the single database constraint at the type level, instead of using a runtime check?

We can define a type for the database options, like this:

type DatabaseOption =
    | {
        /**
         * D1 database configuration for SQLite
         */
        d1: DrizzleConfig<typeof d1Drizzle>;
        postgres?: never;
        mysql?: never;
    }
    | {
        /**
         * Postgres database configuration for Hyperdrive
         */
        postgres: DrizzleConfig<typeof postgresDrizzle>;
        d1?: never;
        mysql?: never;
    }
    | {
        /**
         * MySQL database configuration for Hyperdrive
         */
        mysql: DrizzleConfig<typeof mysqlDrizzle>;
        d1?: never;
        postgres?: never;
    };

By applying this DatabaseOption type to WithCloudflareOptions, we can guarantee that only one database configuration is provided at compile time, eliminating the need for the runtime check.

The final type would look like this:

type WithCloudflareOptions = CloudflarePluginOptions &
    Partial<DatabaseOption> & {
        /**
         * KV namespace for secondary storage, if you want to use that.
         */
        kv?: KVNamespace<string>;
    };

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@imjlk Great feedback! I've been thinking about this too... I wasn't sure which would be clearer to user of the plugin.

Copy link
Contributor

@imjlk imjlk Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, if it's for user feedback, the original method would be better. Also, I did a quick build with the code I wrote and didn't encounter any type errors when even if i set up multiple databases 😞
However, I think it's not common case to use multiple databases

return {
...options,
database: cloudFlareOptions.d1
? drizzleAdapter(cloudFlareOptions.d1.db, {
provider: "sqlite",
...cloudFlareOptions.d1.options,
})
: undefined,
database,
secondaryStorage: cloudFlareOptions.kv ? createKVStorage(cloudFlareOptions.kv) : undefined,
plugins: [cloudflare(cloudFlareOptions), ...(options.plugins ?? [])],
advanced: updatedAdvanced,
Expand Down
41 changes: 29 additions & 12 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import type { KVNamespace } from "@cloudflare/workers-types";
import type { AuthContext } from "better-auth";
import type { DrizzleAdapterConfig } from "better-auth/adapters/drizzle";
import type { FieldAttribute } from "better-auth/db";
import type { drizzle } from "drizzle-orm/d1";
import type { drizzle as d1Drizzle } from "drizzle-orm/d1";
import type { drizzle as postgresDrizzle } from "drizzle-orm/postgres-js";
import type { drizzle as mysqlDrizzle } from "drizzle-orm/mysql2";

export interface CloudflarePluginOptions {
/**
Expand All @@ -29,20 +31,35 @@ export interface CloudflarePluginOptions {
r2?: R2Config;
}

/**
* Generic drizzle database configuration
*/
export type DrizzleConfig<T extends typeof d1Drizzle | typeof postgresDrizzle | typeof mysqlDrizzle> = {
/**
* The drizzle database instance
*/
db: ReturnType<T>;
/**
* Drizzle adapter options
*/
options?: Omit<DrizzleAdapterConfig, "provider">;
};

export interface WithCloudflareOptions extends CloudflarePluginOptions {
/**
* D1 database for primary storage, if that's what you're using.
* D1 database configuration for SQLite
*/
d1?: {
/**
* D1 database for primary storage, if that's what you're using.
*/
db: ReturnType<typeof drizzle>;
/**
* Drizzle adapter options for primary storage, if you're using D1.
*/
options?: Omit<DrizzleAdapterConfig, "provider">;
};
d1?: DrizzleConfig<typeof d1Drizzle>;

/**
* Postgres database configuration for Hyperdrive
*/
postgres?: DrizzleConfig<typeof postgresDrizzle>;

/**
* MySQL database configuration for Hyperdrive
*/
mysql?: DrizzleConfig<typeof mysqlDrizzle>;

/**
* KV namespace for secondary storage, if you want to use that.
Expand Down