From dd1d46e3a47a5787e9b9004d53b6936b4d9ee468 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Tue, 24 Mar 2026 14:32:45 +0900 Subject: [PATCH 1/2] docs: add Nuxt + Cloudflare Pages + D1 example Add a minimal example demonstrating better-auth with Nuxt on Cloudflare Pages using D1 as the database. Includes email/password authentication, SSR session management, auth middleware, login/signup/dashboard pages, and CLI-based schema generation via `npx auth generate`. --- examples/nuxt-d1-cloudflare/.env.example | 2 + examples/nuxt-d1-cloudflare/.gitignore | 17 +++ examples/nuxt-d1-cloudflare/README.md | 124 ++++++++++++++++++ examples/nuxt-d1-cloudflare/app.vue | 5 + .../nuxt-d1-cloudflare/lib/auth-client.ts | 3 + .../middleware/auth.global.ts | 13 ++ .../nuxt-d1-cloudflare/migrations/.gitkeep | 0 examples/nuxt-d1-cloudflare/nuxt.config.ts | 12 ++ examples/nuxt-d1-cloudflare/package.json | 26 ++++ .../nuxt-d1-cloudflare/pages/dashboard.vue | 27 ++++ examples/nuxt-d1-cloudflare/pages/index.vue | 23 ++++ examples/nuxt-d1-cloudflare/pages/login.vue | 54 ++++++++ examples/nuxt-d1-cloudflare/pages/signup.vue | 60 +++++++++ .../server/api/auth/[...all].ts | 6 + .../nuxt-d1-cloudflare/server/api/me.get.ts | 14 ++ .../nuxt-d1-cloudflare/server/lib/auth.ts | 16 +++ examples/nuxt-d1-cloudflare/tsconfig.json | 3 + examples/nuxt-d1-cloudflare/wrangler.toml | 8 ++ 18 files changed, 413 insertions(+) create mode 100644 examples/nuxt-d1-cloudflare/.env.example create mode 100644 examples/nuxt-d1-cloudflare/.gitignore create mode 100644 examples/nuxt-d1-cloudflare/README.md create mode 100644 examples/nuxt-d1-cloudflare/app.vue create mode 100644 examples/nuxt-d1-cloudflare/lib/auth-client.ts create mode 100644 examples/nuxt-d1-cloudflare/middleware/auth.global.ts create mode 100644 examples/nuxt-d1-cloudflare/migrations/.gitkeep create mode 100644 examples/nuxt-d1-cloudflare/nuxt.config.ts create mode 100644 examples/nuxt-d1-cloudflare/package.json create mode 100644 examples/nuxt-d1-cloudflare/pages/dashboard.vue create mode 100644 examples/nuxt-d1-cloudflare/pages/index.vue create mode 100644 examples/nuxt-d1-cloudflare/pages/login.vue create mode 100644 examples/nuxt-d1-cloudflare/pages/signup.vue create mode 100644 examples/nuxt-d1-cloudflare/server/api/auth/[...all].ts create mode 100644 examples/nuxt-d1-cloudflare/server/api/me.get.ts create mode 100644 examples/nuxt-d1-cloudflare/server/lib/auth.ts create mode 100644 examples/nuxt-d1-cloudflare/tsconfig.json create mode 100644 examples/nuxt-d1-cloudflare/wrangler.toml diff --git a/examples/nuxt-d1-cloudflare/.env.example b/examples/nuxt-d1-cloudflare/.env.example new file mode 100644 index 0000000..fc0e739 --- /dev/null +++ b/examples/nuxt-d1-cloudflare/.env.example @@ -0,0 +1,2 @@ +BETTER_AUTH_SECRET=your-secret-key-at-least-32-characters +BETTER_AUTH_URL=http://localhost:3000 diff --git a/examples/nuxt-d1-cloudflare/.gitignore b/examples/nuxt-d1-cloudflare/.gitignore new file mode 100644 index 0000000..704e4df --- /dev/null +++ b/examples/nuxt-d1-cloudflare/.gitignore @@ -0,0 +1,17 @@ +# Nuxt +.nuxt +.output +.data +.nitro +.cache + +# Node +node_modules + +# Cloudflare +.wrangler +.dev.vars + +# Misc +.DS_Store +*.log diff --git a/examples/nuxt-d1-cloudflare/README.md b/examples/nuxt-d1-cloudflare/README.md new file mode 100644 index 0000000..db20818 --- /dev/null +++ b/examples/nuxt-d1-cloudflare/README.md @@ -0,0 +1,124 @@ +# Nuxt + Cloudflare Pages + D1 Example + +A minimal example of using [Better Auth](https://better-auth.com) with **Nuxt**, **Cloudflare Pages**, and **Cloudflare D1** (SQLite). + +## Features + +- Email & password authentication +- Session management with SSR support +- Cloudflare D1 as the database (better-auth auto-detects D1 bindings) +- Auth middleware for protected routes +- Server-side session access via `auth.api.getSession()` + +## Setup + +### 1. Install dependencies + +```bash +npm install +``` + +### 2. Create a D1 database + +```bash +wrangler d1 create better-auth-db +``` + +Copy the `database_id` from the output and update `wrangler.toml`. + +### 3. Generate & apply schema + +Better Auth CLI auto-generates the SQL schema from your auth config: + +```bash +# Generate schema SQL from your auth config +npm run db:generate + +# Apply to local D1 +npm run db:migrate + +# Apply to remote D1 (production) +npm run db:migrate:remote +``` + +The `db:generate` script runs `npx auth generate --output ./migrations/schema.sql`, which reads your `server/lib/auth.ts` config and outputs the required tables (user, session, account, verification) as SQL. + +### 4. Configure environment + +Copy `.env.example` to `.env` and set your values: + +```bash +cp .env.example .env +``` + +For production, set secrets via Cloudflare dashboard or: + +```bash +wrangler pages secret put BETTER_AUTH_SECRET +wrangler pages secret put BETTER_AUTH_URL +``` + +### 5. Development + +```bash +npm run dev +``` + +### 6. Deploy + +```bash +npm run build +npm run deploy +``` + +## How it works + +### D1 Integration + +Better Auth auto-detects D1 bindings — just pass `env.DB` directly to the `database` option: + +```ts +// server/lib/auth.ts +betterAuth({ + database: cloudflare.env.DB, // auto-detected as D1 +}) +``` + +No need for `kysely-d1` or any extra adapter package. Better Auth uses its built-in `D1SqliteDialect` internally. + +### Cloudflare Pages + Nuxt + +Nuxt is configured with the `cloudflare-pages` Nitro preset. The D1 binding is accessed via `event.context.cloudflare.env.DB` in server routes. + +### Auth Handler + +All auth API routes are handled by a single catch-all route at `server/api/auth/[...all].ts`. + +### Client + +The auth client is created with `better-auth/vue` for Vue/Nuxt reactivity support, including `useSession()` with SSR via `useFetch`. + +## Project Structure + +``` +├── app.vue # Root component +├── pages/ +│ ├── index.vue # Home page +│ ├── login.vue # Sign in form +│ ├── signup.vue # Sign up form +│ └── dashboard.vue # Protected dashboard +├── middleware/ +│ └── auth.global.ts # Auth route guard +├── lib/ +│ └── auth-client.ts # Client-side auth +├── server/ +│ ├── lib/ +│ │ └── auth.ts # Server-side auth config +│ └── api/ +│ ├── auth/[...all].ts # Auth catch-all handler +│ └── me.get.ts # Example protected API route +├── migrations/ +│ └── schema.sql # Generated by `npx auth generate` +├── nuxt.config.ts +└── wrangler.toml # Cloudflare config +``` diff --git a/examples/nuxt-d1-cloudflare/app.vue b/examples/nuxt-d1-cloudflare/app.vue new file mode 100644 index 0000000..f8eacfa --- /dev/null +++ b/examples/nuxt-d1-cloudflare/app.vue @@ -0,0 +1,5 @@ + diff --git a/examples/nuxt-d1-cloudflare/lib/auth-client.ts b/examples/nuxt-d1-cloudflare/lib/auth-client.ts new file mode 100644 index 0000000..4fbbbc9 --- /dev/null +++ b/examples/nuxt-d1-cloudflare/lib/auth-client.ts @@ -0,0 +1,3 @@ +import { createAuthClient } from 'better-auth/vue' + +export const authClient = createAuthClient() diff --git a/examples/nuxt-d1-cloudflare/middleware/auth.global.ts b/examples/nuxt-d1-cloudflare/middleware/auth.global.ts new file mode 100644 index 0000000..ef25265 --- /dev/null +++ b/examples/nuxt-d1-cloudflare/middleware/auth.global.ts @@ -0,0 +1,13 @@ +import { authClient } from '~/lib/auth-client' + +export default defineNuxtRouteMiddleware(async (to) => { + const { data: session } = await authClient.useSession(useFetch) + + if (!session.value && to.path === '/dashboard') { + return navigateTo('/login') + } + + if (session.value && (to.path === '/login' || to.path === '/signup')) { + return navigateTo('/dashboard') + } +}) diff --git a/examples/nuxt-d1-cloudflare/migrations/.gitkeep b/examples/nuxt-d1-cloudflare/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/examples/nuxt-d1-cloudflare/nuxt.config.ts b/examples/nuxt-d1-cloudflare/nuxt.config.ts new file mode 100644 index 0000000..7fe9442 --- /dev/null +++ b/examples/nuxt-d1-cloudflare/nuxt.config.ts @@ -0,0 +1,12 @@ +export default defineNuxtConfig({ + compatibilityDate: '2025-03-24', + + nitro: { + preset: 'cloudflare-pages', + }, + + runtimeConfig: { + betterAuthSecret: process.env.BETTER_AUTH_SECRET || 'development-secret-change-me', + betterAuthUrl: process.env.BETTER_AUTH_URL || 'http://localhost:3000', + }, +}) diff --git a/examples/nuxt-d1-cloudflare/package.json b/examples/nuxt-d1-cloudflare/package.json new file mode 100644 index 0000000..2066cce --- /dev/null +++ b/examples/nuxt-d1-cloudflare/package.json @@ -0,0 +1,26 @@ +{ + "name": "nuxt-d1-cloudflare-example", + "private": true, + "type": "module", + "scripts": { + "dev": "nuxt dev --dotenv .env", + "build": "nuxt build", + "preview": "wrangler pages dev", + "deploy": "wrangler pages deploy", + "db:generate": "npx auth generate --output ./migrations/schema.sql", + "db:migrate": "wrangler d1 execute better-auth-db --local --file=./migrations/schema.sql", + "db:migrate:remote": "wrangler d1 execute better-auth-db --remote --file=./migrations/schema.sql", + "postinstall": "nuxt prepare" + }, + "dependencies": { + "better-auth": "^1.2.0", + "nuxt": "^3.16.0", + "vue": "^3.5.0", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250313.0", + "typescript": "^5.8.0", + "wrangler": "^4.6.0" + } +} diff --git a/examples/nuxt-d1-cloudflare/pages/dashboard.vue b/examples/nuxt-d1-cloudflare/pages/dashboard.vue new file mode 100644 index 0000000..63c094f --- /dev/null +++ b/examples/nuxt-d1-cloudflare/pages/dashboard.vue @@ -0,0 +1,27 @@ + + + diff --git a/examples/nuxt-d1-cloudflare/pages/index.vue b/examples/nuxt-d1-cloudflare/pages/index.vue new file mode 100644 index 0000000..b07715e --- /dev/null +++ b/examples/nuxt-d1-cloudflare/pages/index.vue @@ -0,0 +1,23 @@ + + + diff --git a/examples/nuxt-d1-cloudflare/pages/login.vue b/examples/nuxt-d1-cloudflare/pages/login.vue new file mode 100644 index 0000000..63a477b --- /dev/null +++ b/examples/nuxt-d1-cloudflare/pages/login.vue @@ -0,0 +1,54 @@ + + + diff --git a/examples/nuxt-d1-cloudflare/pages/signup.vue b/examples/nuxt-d1-cloudflare/pages/signup.vue new file mode 100644 index 0000000..127448f --- /dev/null +++ b/examples/nuxt-d1-cloudflare/pages/signup.vue @@ -0,0 +1,60 @@ + + + diff --git a/examples/nuxt-d1-cloudflare/server/api/auth/[...all].ts b/examples/nuxt-d1-cloudflare/server/api/auth/[...all].ts new file mode 100644 index 0000000..841aff2 --- /dev/null +++ b/examples/nuxt-d1-cloudflare/server/api/auth/[...all].ts @@ -0,0 +1,6 @@ +import { getAuth } from '~/server/lib/auth' + +export default defineEventHandler((event) => { + const auth = getAuth(event) + return auth.handler(toWebRequest(event)) +}) diff --git a/examples/nuxt-d1-cloudflare/server/api/me.get.ts b/examples/nuxt-d1-cloudflare/server/api/me.get.ts new file mode 100644 index 0000000..7b80493 --- /dev/null +++ b/examples/nuxt-d1-cloudflare/server/api/me.get.ts @@ -0,0 +1,14 @@ +import { getAuth } from '~/server/lib/auth' + +export default defineEventHandler(async (event) => { + const auth = getAuth(event) + const session = await auth.api.getSession({ + headers: event.headers, + }) + + if (!session) { + throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }) + } + + return { user: session.user } +}) diff --git a/examples/nuxt-d1-cloudflare/server/lib/auth.ts b/examples/nuxt-d1-cloudflare/server/lib/auth.ts new file mode 100644 index 0000000..fa4b5f9 --- /dev/null +++ b/examples/nuxt-d1-cloudflare/server/lib/auth.ts @@ -0,0 +1,16 @@ +import { betterAuth } from 'better-auth' +import type { H3Event } from 'h3' + +export function getAuth(event: H3Event) { + const { cloudflare } = event.context + const config = useRuntimeConfig(event) + + return betterAuth({ + database: cloudflare.env.DB, + secret: config.betterAuthSecret, + baseURL: config.betterAuthUrl, + emailAndPassword: { + enabled: true, + }, + }) +} diff --git a/examples/nuxt-d1-cloudflare/tsconfig.json b/examples/nuxt-d1-cloudflare/tsconfig.json new file mode 100644 index 0000000..4b34df1 --- /dev/null +++ b/examples/nuxt-d1-cloudflare/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.nuxt/tsconfig.json" +} diff --git a/examples/nuxt-d1-cloudflare/wrangler.toml b/examples/nuxt-d1-cloudflare/wrangler.toml new file mode 100644 index 0000000..fe9def4 --- /dev/null +++ b/examples/nuxt-d1-cloudflare/wrangler.toml @@ -0,0 +1,8 @@ +name = "nuxt-d1-cloudflare-example" +compatibility_date = "2025-03-24" +pages_build_output_dir = ".output/public" + +[[d1_databases]] +binding = "DB" +database_name = "better-auth-db" +database_id = "" From 030676101dcef9426ca636e0a04b01699c425b27 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Tue, 24 Mar 2026 14:54:28 +0900 Subject: [PATCH 2/2] fix: address review issues in nuxt-d1-cloudflare example - Remove hardcoded fallback secret, add runtime guard for missing BETTER_AUTH_SECRET - Fix middleware to only run on client side (import.meta.client guard) - Protect all /dashboard sub-routes with startsWith - Update README code snippet to show per-request getAuth(event) pattern --- examples/nuxt-d1-cloudflare/README.md | 15 ++++++++++++--- .../nuxt-d1-cloudflare/middleware/auth.global.ts | 6 +++++- examples/nuxt-d1-cloudflare/nuxt.config.ts | 2 +- examples/nuxt-d1-cloudflare/server/lib/auth.ts | 4 ++++ 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/examples/nuxt-d1-cloudflare/README.md b/examples/nuxt-d1-cloudflare/README.md index db20818..7815584 100644 --- a/examples/nuxt-d1-cloudflare/README.md +++ b/examples/nuxt-d1-cloudflare/README.md @@ -79,11 +79,20 @@ Better Auth auto-detects D1 bindings — just pass `env.DB` directly to the `dat ```ts // server/lib/auth.ts -betterAuth({ - database: cloudflare.env.DB, // auto-detected as D1 -}) +export function getAuth(event: H3Event) { + const { cloudflare } = event.context + const config = useRuntimeConfig(event) + + return betterAuth({ + database: cloudflare.env.DB, // auto-detected as D1 + secret: config.betterAuthSecret, + baseURL: config.betterAuthUrl, + }) +} ``` +Because Cloudflare Pages exposes the D1 binding only on the incoming request context, `betterAuth` is instantiated per request inside `getAuth(event)` rather than at module level. + No need for `kysely-d1` or any extra adapter package. Better Auth uses its built-in `D1SqliteDialect` internally. ### Cloudflare Pages + Nuxt diff --git a/examples/nuxt-d1-cloudflare/middleware/auth.global.ts b/examples/nuxt-d1-cloudflare/middleware/auth.global.ts index ef25265..7f39fa5 100644 --- a/examples/nuxt-d1-cloudflare/middleware/auth.global.ts +++ b/examples/nuxt-d1-cloudflare/middleware/auth.global.ts @@ -1,9 +1,13 @@ import { authClient } from '~/lib/auth-client' export default defineNuxtRouteMiddleware(async (to) => { + if (!import.meta.client) { + return + } + const { data: session } = await authClient.useSession(useFetch) - if (!session.value && to.path === '/dashboard') { + if (!session.value && to.path.startsWith('/dashboard')) { return navigateTo('/login') } diff --git a/examples/nuxt-d1-cloudflare/nuxt.config.ts b/examples/nuxt-d1-cloudflare/nuxt.config.ts index 7fe9442..0b58570 100644 --- a/examples/nuxt-d1-cloudflare/nuxt.config.ts +++ b/examples/nuxt-d1-cloudflare/nuxt.config.ts @@ -6,7 +6,7 @@ export default defineNuxtConfig({ }, runtimeConfig: { - betterAuthSecret: process.env.BETTER_AUTH_SECRET || 'development-secret-change-me', + betterAuthSecret: process.env.BETTER_AUTH_SECRET || '', betterAuthUrl: process.env.BETTER_AUTH_URL || 'http://localhost:3000', }, }) diff --git a/examples/nuxt-d1-cloudflare/server/lib/auth.ts b/examples/nuxt-d1-cloudflare/server/lib/auth.ts index fa4b5f9..67c72b6 100644 --- a/examples/nuxt-d1-cloudflare/server/lib/auth.ts +++ b/examples/nuxt-d1-cloudflare/server/lib/auth.ts @@ -5,6 +5,10 @@ export function getAuth(event: H3Event) { const { cloudflare } = event.context const config = useRuntimeConfig(event) + if (!config.betterAuthSecret) { + throw new Error('BETTER_AUTH_SECRET is not set. Configure it as an environment variable.') + } + return betterAuth({ database: cloudflare.env.DB, secret: config.betterAuthSecret,