Database

Cloudflare D1, Drizzle ORM, SQLite schemas, and reviewed migrations.

The database layer is Integrated with Cloudflare D1 (serverless SQLite) and Drizzle ORM's D1 driver. The repository includes auth and payment schemas plus a generated SQL migration.

Important files

FileRole
src/lib/db/index.tsRequest-time D1 client and exported lazy db proxy
src/lib/db/schema/auth.tsBetter Auth user, session, account, and verification tables
src/lib/db/schema/payment.tsPayment and subscription tables
drizzle.config.tsSQLite schema path and optional d1-http tooling configuration
drizzle/Generated, reviewed SQL migrations and Drizzle metadata
wrangler.jsoncLocal/production DB binding and migrations directory

Request-time D1 binding

src/lib/db/index.ts imports env from cloudflare:workers and creates the client with drizzle(env.DB, { schema }). The exported db is a lazy Proxy, so env.DB is first read during a request rather than during module evaluation.

DB is a Cloudflare binding, not a connection string. There is no DATABASE_URL, and runtime code must not read process.env.DB. Preserve the request-time factory/proxy pattern when reorganizing the database module.

SQLite schema conventions

Both schema files use drizzle-orm/sqlite-core:

  • Primary keys and most identifiers are text; application-created payment IDs use crypto.randomUUID() through $defaultFn.
  • Booleans use integer(..., { mode: "boolean" }).
  • Dates use integer(..., { mode: "timestamp_ms" }) and app-side Date defaults through $defaultFn.
  • Constrained string values such as payment status, provider, and plan interval use SQLite text enums for TypeScript inference.
  • User-dependent rows reference user.id with onDelete: "cascade".
  • Unique constraints and explicit indexes cover session tokens, email, order numbers, user payment lookup, and subscription status.

The auth schema must continue to match the fields expected by Better Auth and its admin plugin. When changing users, sessions, accounts, or verification, update src/lib/auth.ts and the adapter schema in the same review.

Migration workflow

Change the TypeScript schema

Edit a file under src/lib/db/schema/. If you add a schema module, import it in src/lib/db/index.ts and spread it into Drizzle's schema object.

Generate SQL offline

pnpm db:generate

This compares the schema with Drizzle metadata and writes a migration under drizzle/. It does not require a D1 connection.

Review and commit the migration

Inspect the generated SQL for destructive changes, correct defaults, indexes, foreign keys, and data backfills. Commit the SQL and drizzle/meta changes with the schema code.

Apply locally

pnpm db:migrate:local

This runs wrangler d1 migrations apply soar-tanstack --local against the Miniflare database.

Apply remotely

After review and backup:

pnpm db:migrate

This runs wrangler d1 migrations apply soar-tanstack --remote. If you rename the D1 database, update both wrangler.jsonc and the package scripts.

Do not use drizzle-kit push. This template intentionally uses generated, reviewed, committed migrations applied by Wrangler.

Drizzle configuration

drizzle.config.ts sets:

export default defineConfig({
	out: "./drizzle",
	schema: "./src/lib/db/schema",
	dialect: "sqlite",
	driver: "d1-http",
	// dbCredentials: Cloudflare account, database, and D1-scoped token
});

The HTTP credentials are only for optional Drizzle Kit operations that contact remote D1. The config loads .env.local; shell exports also work. Migration application uses the Wrangler scripts above.

Transactions and multi-statement writes

D1 does not support the interactive transaction model expected by a typical long-lived SQL connection. Do not build product logic around db.transaction(async (tx) => ...). Prepare independent Drizzle statements and use db.batch([...]) for atomic batched execution, keeping external network calls outside the batch.

Design idempotent writes for retried requests and webhooks. The payment webhook, for example, upserts a subscription on the unique userId key so duplicate deliveries converge.

Production considerations

  • Apply and verify migrations in a non-production D1 database first.
  • Export or back up production data before a destructive migration; define a restore procedure and retention policy.
  • Check current D1 database size, query, and batch limits against your workload in Cloudflare before launch.
  • Keep indexes aligned with real query patterns and inspect slow or high-volume paths through Workers observability.
  • Be deliberate about D1 read consistency if you enable read replication; a user flow that immediately reads its own write may require a primary/session consistency strategy.
  • Treat migration SQL as application code: review it and never edit a migration that has already been applied to a shared environment.

Verify

  • pnpm db:generate reports no unexpected schema drift.
  • pnpm db:migrate:local succeeds from a fresh local database.
  • Registration writes user/session/auth records to local D1.
  • A test checkout creates a payment row and a signed webhook updates it.
  • The same reviewed migration applies to a staging remote D1 database.

On this page