From b1d0267bf1be05cc1f66793d2438337cff481157 Mon Sep 17 00:00:00 2001 From: Daniel Kirby Date: Thu, 25 Jun 2026 11:40:56 +0100 Subject: [PATCH 1/3] feat: add D1 runtime database binding --- src/lib/server/auth.ts | 17 +++++++-- src/lib/server/db/index.ts | 76 +++++++++++++++++++++++++++++++++++--- worker-configuration.d.ts | 6 +-- wrangler.jsonc | 8 ++++ 4 files changed, 94 insertions(+), 13 deletions(-) diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 460b3ad..6270893 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -3,13 +3,24 @@ import { drizzleAdapter } from 'better-auth/adapters/drizzle'; import { admin } from 'better-auth/plugins'; import { sveltekitCookies } from 'better-auth/svelte-kit'; import { env } from '$env/dynamic/private'; +import { building } from '$app/environment'; import { getRequestEvent } from '$app/server'; import { db } from '$lib/server/db'; +import * as schema from '$lib/server/db/schema'; + +const buildTimeOrigin = 'http://localhost:5173'; +const buildTimeSecret = 'clearity-build-time-placeholder-secret'; + +const origin = env.ORIGIN ?? (building ? buildTimeOrigin : undefined); +const secret = env.BETTER_AUTH_SECRET ?? (building ? buildTimeSecret : undefined); + +if (!origin) throw new Error('ORIGIN is not set'); +if (!secret) throw new Error('BETTER_AUTH_SECRET is not set'); export const auth = betterAuth({ - baseURL: env.ORIGIN, - secret: env.BETTER_AUTH_SECRET, - database: drizzleAdapter(db, { provider: 'sqlite' }), + baseURL: origin, + secret, + database: drizzleAdapter(db, { provider: 'sqlite', schema }), emailAndPassword: { enabled: true, disableSignUp: true diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts index 662fc2d..a20616e 100644 --- a/src/lib/server/db/index.ts +++ b/src/lib/server/db/index.ts @@ -1,10 +1,74 @@ -import { drizzle } from 'drizzle-orm/libsql'; -import { createClient } from '@libsql/client'; -import * as schema from './schema'; import { env } from '$env/dynamic/private'; +import { getRequestEvent } from '$app/server'; +import { createClient, type Client } from '@libsql/client'; +import { drizzle as drizzleD1, type DrizzleD1Database } from 'drizzle-orm/d1'; +import { drizzle as drizzleLibSQL, type LibSQLDatabase } from 'drizzle-orm/libsql'; +import * as schema from './schema'; -if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set'); +type Schema = typeof schema; +type LocalDatabase = LibSQLDatabase & { $client: Client }; +type D1DatabaseClient = DrizzleD1Database & { $client: D1Database }; -const client = createClient({ url: env.DATABASE_URL }); +export type Database = LocalDatabase; +export type RuntimeDatabase = LocalDatabase | D1DatabaseClient; -export const db = drizzle(client, { schema }); +let localDb: LocalDatabase | undefined; +const d1Databases = new WeakMap(); + +function getD1Binding() { + try { + return getRequestEvent().platform?.env.DB; + } catch { + return undefined; + } +} + +function getLocalDb() { + if (localDb) return localDb; + if (!env.DATABASE_URL) + throw new Error('DATABASE_URL is not set and no Cloudflare D1 DB binding is available'); + + const client = createClient({ url: env.DATABASE_URL }); + localDb = drizzleLibSQL(client, { schema }); + + return localDb; +} + +function getD1Db(binding: D1Database) { + const cached = d1Databases.get(binding); + if (cached) return cached; + + const database = drizzleD1(binding, { schema }); + d1Databases.set(binding, database); + + return database; +} + +export function getDb(): RuntimeDatabase { + if (env.DATABASE_URL) return getLocalDb(); + + const d1Binding = getD1Binding(); + if (d1Binding) return getD1Db(d1Binding); + + return getLocalDb(); +} + +export const db = new Proxy({} as Database, { + get(_target, property) { + const database = getDb(); + const value = Reflect.get(database, property); + return typeof value === 'function' ? value.bind(database) : value; + }, + getOwnPropertyDescriptor(_target, property) { + return Reflect.getOwnPropertyDescriptor(getDb(), property); + }, + getPrototypeOf() { + return Reflect.getPrototypeOf(getDb()); + }, + has(_target, property) { + return property in getDb(); + }, + ownKeys() { + return Reflect.ownKeys(getDb()); + } +}); diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index b28b382..eb711af 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,11 +1,9 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 61c64c9cd1c2465ff3a92bc5ac82d57d) +// Generated by Wrangler by running `wrangler types` (hash: f6e835394237f01fba038726e2d4f9ae) // Runtime types generated with workerd@1.20260521.1 2026-05-23 nodejs_als interface __BaseEnv_Env { + DB: D1Database; ASSETS: Fetcher; - DATABASE_URL: string; - ORIGIN: string; - BETTER_AUTH_SECRET: string; } declare namespace Cloudflare { interface Env extends __BaseEnv_Env {} diff --git a/wrangler.jsonc b/wrangler.jsonc index bd168bd..35b7afb 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -8,6 +8,14 @@ "binding": "ASSETS", "directory": ".svelte-kit/cloudflare" }, + "d1_databases": [ + { + "binding": "DB", + "database_name": "clearity-production", + "database_id": "", + "migrations_dir": "drizzle" + } + ], "workers_dev": true, "preview_urls": true } From 29ff3265e8c7af395106ab1f54699612598df0f5 Mon Sep 17 00:00:00 2001 From: Daniel Kirby Date: Thu, 25 Jun 2026 11:41:00 +0100 Subject: [PATCH 2/3] docs: document D1 deployment flow --- README.md | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e92868e..2580988 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Clearity is a workspace, client, and contract management platform for managed office operations. It tracks clients, contacts, addresses, rooms, services, bookings, contracts, invoices, and workspace admins from a SvelteKit dashboard. -The app is built with SvelteKit, Svelte 5, shadcn-svelte, Superforms, Better Auth, Drizzle ORM, SQLite/libSQL, and Cloudflare Workers. +The app is built with SvelteKit, Svelte 5, shadcn-svelte, Superforms, Better Auth, Drizzle ORM, SQLite/libSQL for local development, Cloudflare D1 for production, and Cloudflare Workers. ## Development @@ -52,19 +52,26 @@ pnpm db:studio # Open Drizzle Studio pnpm gen # Regenerate Cloudflare Worker types ``` -## Environment Variables +## Environment Variables And Bindings Required: -- `DATABASE_URL`: SQLite/libSQL connection URL. Use `file:local.db` for local development. +- `DATABASE_URL`: SQLite/libSQL connection URL for local development. Use `file:local.db`. - `ORIGIN`: Public app origin, for example `http://localhost:5173` locally or `https://clearity.example.com` in production. - `BETTER_AUTH_SECRET`: Secret used by Better Auth. +Production also requires the Cloudflare D1 binding named `DB`, configured in `wrangler.jsonc`. Do not set `DATABASE_URL` in production unless you intentionally want to use a hosted libSQL database instead of D1. + ## Database The application schema lives in `src/lib/server/db`. Drizzle migration files live in `drizzle`. -Local development currently uses the libSQL client through `DATABASE_URL`, so the fastest local database is a SQLite file: +Runtime database selection is: + +- If `DATABASE_URL` is set, Clearity uses the libSQL driver. +- If `DATABASE_URL` is not set and the Cloudflare `DB` binding exists, Clearity uses Drizzle's Cloudflare D1 driver. + +The fastest local database is a SQLite file: ```sh DATABASE_URL=file:local.db pnpm db:migrate @@ -77,6 +84,14 @@ pnpm db:generate DATABASE_URL=file:local.db pnpm db:migrate ``` +To test against Wrangler's local D1 simulator instead, leave `DATABASE_URL` unset, apply migrations locally, and run the app through the Cloudflare Worker build: + +```sh +pnpm build +pnpm wrangler d1 migrations apply clearity-production --local +pnpm wrangler dev +``` + ## Production Deployment The Worker is configured in `wrangler.jsonc` and built with the SvelteKit Cloudflare adapter. @@ -91,10 +106,10 @@ pnpm wrangler whoami Create a production D1 database: ```sh -pnpm wrangler d1 create clearity-production +pnpm wrangler d1 create clearity-production --binding DB ``` -Wrangler prints a `database_id`. Add it to `wrangler.jsonc`: +Wrangler prints a `database_id`. Replace the placeholder in `wrangler.jsonc`: ```jsonc { @@ -102,7 +117,8 @@ Wrangler prints a `database_id`. Add it to `wrangler.jsonc`: { "binding": "DB", "database_name": "clearity-production", - "database_id": "" + "database_id": "", + "migrations_dir": "drizzle" } ] } @@ -117,9 +133,7 @@ pnpm gen Apply SQL migrations to the remote D1 database: ```sh -for file in drizzle/*.sql; do - pnpm wrangler d1 execute clearity-production --remote --file "$file" -done +pnpm wrangler d1 migrations apply clearity-production --remote ``` Set production secrets: @@ -129,11 +143,7 @@ pnpm wrangler secret put ORIGIN pnpm wrangler secret put BETTER_AUTH_SECRET ``` -This codebase currently reads the database through `DATABASE_URL`. If production is deployed against Cloudflare D1, wire the runtime database client to the `DB` D1 binding before deploying. If production is deployed against a hosted libSQL database instead, set `DATABASE_URL` as a Worker secret: - -```sh -pnpm wrangler secret put DATABASE_URL -``` +Production uses the `DB` binding by default. `DATABASE_URL` is only needed as a production secret if you intentionally deploy against a hosted libSQL database instead of D1. Build and deploy: From de08587b77fce74b9480c8e4a39b872b40681c5c Mon Sep 17 00:00:00 2001 From: Daniel Kirby Date: Thu, 25 Jun 2026 12:09:44 +0100 Subject: [PATCH 3/3] feat: use D1 binding for local database --- .env.example | 3 --- README.md | 34 +++++++++------------------------- drizzle.config.ts | 3 --- package.json | 5 ++--- pnpm-lock.yaml | 24 ++++++++++++++++-------- src/lib/server/db/index.ts | 28 ++++------------------------ worker-configuration.d.ts | 4 +++- wrangler.jsonc | 3 +++ 8 files changed, 37 insertions(+), 67 deletions(-) diff --git a/.env.example b/.env.example index ced41bb..f97cfd0 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,3 @@ -# Drizzle -DATABASE_URL=file:local.db - ORIGIN="" # Better Auth diff --git a/README.md b/README.md index 2580988..420c3bc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Clearity is a workspace, client, and contract management platform for managed office operations. It tracks clients, contacts, addresses, rooms, services, bookings, contracts, invoices, and workspace admins from a SvelteKit dashboard. -The app is built with SvelteKit, Svelte 5, shadcn-svelte, Superforms, Better Auth, Drizzle ORM, SQLite/libSQL for local development, Cloudflare D1 for production, and Cloudflare Workers. +The app is built with SvelteKit, Svelte 5, shadcn-svelte, Superforms, Better Auth, Drizzle ORM, Cloudflare D1, and Cloudflare Workers. ## Development @@ -15,7 +15,6 @@ pnpm install Create a local `.env` file: ```sh -DATABASE_URL=file:local.db ORIGIN=http://localhost:5173 BETTER_AUTH_SECRET=replace-with-a-long-random-secret ``` @@ -29,7 +28,7 @@ openssl rand -base64 32 Apply database migrations: ```sh -DATABASE_URL=file:local.db pnpm db:migrate +pnpm db:migrate:local ``` Start the development server: @@ -47,8 +46,8 @@ pnpm check # Wrangler types, SvelteKit sync, and svelte-check pnpm lint # Prettier check and ESLint pnpm format # Format the codebase pnpm db:generate # Generate Drizzle migrations after schema changes -pnpm db:migrate # Apply Drizzle migrations to DATABASE_URL -pnpm db:studio # Open Drizzle Studio +pnpm db:migrate:local # Apply migrations to the local D1 database +pnpm db:migrate:remote # Apply migrations to the remote D1 database pnpm gen # Regenerate Cloudflare Worker types ``` @@ -56,40 +55,27 @@ pnpm gen # Regenerate Cloudflare Worker types Required: -- `DATABASE_URL`: SQLite/libSQL connection URL for local development. Use `file:local.db`. - `ORIGIN`: Public app origin, for example `http://localhost:5173` locally or `https://clearity.example.com` in production. - `BETTER_AUTH_SECRET`: Secret used by Better Auth. +- Cloudflare D1 binding `DB`, configured in `wrangler.jsonc`. -Production also requires the Cloudflare D1 binding named `DB`, configured in `wrangler.jsonc`. Do not set `DATABASE_URL` in production unless you intentionally want to use a hosted libSQL database instead of D1. +Local development uses the same `DB` binding shape as production. SvelteKit's Cloudflare adapter populates `platform.env.DB` from `wrangler.jsonc` during dev and preview, and Wrangler stores local D1 data in its local state directory. ## Database The application schema lives in `src/lib/server/db`. Drizzle migration files live in `drizzle`. -Runtime database selection is: - -- If `DATABASE_URL` is set, Clearity uses the libSQL driver. -- If `DATABASE_URL` is not set and the Cloudflare `DB` binding exists, Clearity uses Drizzle's Cloudflare D1 driver. - -The fastest local database is a SQLite file: +Clearity always uses Drizzle's Cloudflare D1 driver through the `DB` binding. Apply migrations to the local D1 database before starting the app: ```sh -DATABASE_URL=file:local.db pnpm db:migrate +pnpm db:migrate:local ``` For schema changes: ```sh pnpm db:generate -DATABASE_URL=file:local.db pnpm db:migrate -``` - -To test against Wrangler's local D1 simulator instead, leave `DATABASE_URL` unset, apply migrations locally, and run the app through the Cloudflare Worker build: - -```sh -pnpm build -pnpm wrangler d1 migrations apply clearity-production --local -pnpm wrangler dev +pnpm db:migrate:local ``` ## Production Deployment @@ -143,8 +129,6 @@ pnpm wrangler secret put ORIGIN pnpm wrangler secret put BETTER_AUTH_SECRET ``` -Production uses the `DB` binding by default. `DATABASE_URL` is only needed as a production secret if you intentionally deploy against a hosted libSQL database instead of D1. - Build and deploy: ```sh diff --git a/drizzle.config.ts b/drizzle.config.ts index 317f310..55e8beb 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,11 +1,8 @@ import { defineConfig } from 'drizzle-kit'; -if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set'); - export default defineConfig({ schema: './src/lib/server/db/schema.ts', dialect: 'sqlite', - dbCredentials: { url: process.env.DATABASE_URL }, verbose: true, strict: true }); diff --git a/package.json b/package.json index abfa2ee..cd841e9 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "lint": "prettier --check . && eslint .", "format": "prettier --write .", "gen": "wrangler types", - "db:push": "drizzle-kit push", "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate", + "db:migrate:local": "wrangler d1 migrations apply clearity-production --local", + "db:migrate:remote": "wrangler d1 migrations apply clearity-production --remote", "auth:schema": "better-auth generate --config src/lib/server/auth.ts --output src/lib/server/db/auth.schema.ts --yes" }, "devDependencies": { @@ -24,7 +24,6 @@ "@eslint/js": "latest", "@fontsource-variable/geist": "^5.2.9", "@internationalized/date": "^3.12.1", - "@libsql/client": "^0.17.2", "@lucide/svelte": "^1.16.0", "@sveltejs/adapter-cloudflare": "^7.2.8", "@sveltejs/kit": "^2.57.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9ff036..d6c20b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,9 +23,6 @@ importers: '@internationalized/date': specifier: ^3.12.1 version: 3.12.1 - '@libsql/client': - specifier: ^0.17.2 - version: 0.17.3 '@lucide/svelte': specifier: ^1.16.0 version: 1.16.0(svelte@5.55.9(@typescript-eslint/types@8.59.4)) @@ -4759,10 +4756,12 @@ snapshots: transitivePeerDependencies: - bufferutil - utf-8-validate + optional: true '@libsql/core@0.17.3': dependencies: js-base64: 3.7.8 + optional: true '@libsql/darwin-arm64@0.5.29': optional: true @@ -4777,6 +4776,7 @@ snapshots: transitivePeerDependencies: - bufferutil - utf-8-validate + optional: true '@libsql/isomorphic-ws@0.1.5': dependencies: @@ -4785,6 +4785,7 @@ snapshots: transitivePeerDependencies: - bufferutil - utf-8-validate + optional: true '@libsql/linux-arm-gnueabihf@0.5.29': optional: true @@ -4823,7 +4824,8 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true - '@neon-rs/load@0.0.4': {} + '@neon-rs/load@0.0.4': + optional: true '@noble/ciphers@2.2.0': {} @@ -5072,6 +5074,7 @@ snapshots: '@types/ws@8.18.1': dependencies: '@types/node': 24.12.4 + optional: true '@typeschema/class-validator@0.3.0(@types/json-schema@7.0.15)(class-validator@0.14.4)': dependencies: @@ -5557,7 +5560,8 @@ snapshots: destr@2.0.5: {} - detect-libc@2.0.2: {} + detect-libc@2.0.2: + optional: true detect-libc@2.1.2: {} @@ -5976,7 +5980,8 @@ snapshots: jose@6.2.3: {} - js-base64@3.7.8: {} + js-base64@3.7.8: + optional: true js-tokens@4.0.0: {} @@ -6066,6 +6071,7 @@ snapshots: '@libsql/linux-x64-gnu': 0.5.29 '@libsql/linux-x64-musl': 0.5.29 '@libsql/win32-x64-msvc': 0.5.29 + optional: true lightningcss-android-arm64@1.32.0: optional: true @@ -6356,7 +6362,8 @@ snapshots: prettier@3.8.3: {} - promise-limit@2.7.0: {} + promise-limit@2.7.0: + optional: true prompts@2.4.2: dependencies: @@ -6878,7 +6885,8 @@ snapshots: ws@8.20.1: {} - ws@8.21.0: {} + ws@8.21.0: + optional: true wsl-utils@0.1.0: dependencies: diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts index a20616e..91516b3 100644 --- a/src/lib/server/db/index.ts +++ b/src/lib/server/db/index.ts @@ -1,19 +1,12 @@ -import { env } from '$env/dynamic/private'; import { getRequestEvent } from '$app/server'; -import { createClient, type Client } from '@libsql/client'; import { drizzle as drizzleD1, type DrizzleD1Database } from 'drizzle-orm/d1'; -import { drizzle as drizzleLibSQL, type LibSQLDatabase } from 'drizzle-orm/libsql'; import * as schema from './schema'; type Schema = typeof schema; -type LocalDatabase = LibSQLDatabase & { $client: Client }; -type D1DatabaseClient = DrizzleD1Database & { $client: D1Database }; -export type Database = LocalDatabase; -export type RuntimeDatabase = LocalDatabase | D1DatabaseClient; +export type Database = DrizzleD1Database & { $client: D1Database }; -let localDb: LocalDatabase | undefined; -const d1Databases = new WeakMap(); +const d1Databases = new WeakMap(); function getD1Binding() { try { @@ -23,17 +16,6 @@ function getD1Binding() { } } -function getLocalDb() { - if (localDb) return localDb; - if (!env.DATABASE_URL) - throw new Error('DATABASE_URL is not set and no Cloudflare D1 DB binding is available'); - - const client = createClient({ url: env.DATABASE_URL }); - localDb = drizzleLibSQL(client, { schema }); - - return localDb; -} - function getD1Db(binding: D1Database) { const cached = d1Databases.get(binding); if (cached) return cached; @@ -44,13 +26,11 @@ function getD1Db(binding: D1Database) { return database; } -export function getDb(): RuntimeDatabase { - if (env.DATABASE_URL) return getLocalDb(); - +export function getDb(): Database { const d1Binding = getD1Binding(); if (d1Binding) return getD1Db(d1Binding); - return getLocalDb(); + throw new Error('Cloudflare D1 DB binding is not available'); } export const db = new Proxy({} as Database, { diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index eb711af..e8a4b20 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,9 +1,11 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: f6e835394237f01fba038726e2d4f9ae) +// Generated by Wrangler by running `wrangler types` (hash: 36a638c2dc42c34e7d5ddc208fd03581) // Runtime types generated with workerd@1.20260521.1 2026-05-23 nodejs_als interface __BaseEnv_Env { DB: D1Database; ASSETS: Fetcher; + ORIGIN: string; + BETTER_AUTH_SECRET: string; } declare namespace Cloudflare { interface Env extends __BaseEnv_Env {} diff --git a/wrangler.jsonc b/wrangler.jsonc index 35b7afb..d4f82cc 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -16,6 +16,9 @@ "migrations_dir": "drizzle" } ], + "secrets": { + "required": ["ORIGIN", "BETTER_AUTH_SECRET"] + }, "workers_dev": true, "preview_urls": true }