@onyx/db
Drizzle schema and typed data access on Cloudflare D1.
@onyx/db owns the database layer: a single Drizzle schema covering every table (including the Better Auth tables) and a factory that wraps the D1 binding in a fully typed Drizzle client. Any code running in the Worker creates a client from the DB binding and gets end-to-end types from the schema.
Exports
@onyx/db:createDb(d1), theDatabasetype, and aschemare-export.@onyx/db/schema: every table definition, importable individually.@onyx/db/seed-data: deterministic seed constants (seedUser,seedOrganization,seedInvitation) shared with the e2e tests.
Usage
Create a client from the DB binding (already declared for the Worker, and simulated automatically in local dev) and query with the Drizzle relational API:
import { createDb, schema } from '@onyx/db';
import { env } from 'cloudflare:workers';
import { eq } from 'drizzle-orm';
const db = createDb(env.DB);
const organization = await db.query.organization.findFirst({
where: eq(schema.organization.id, context.organizationId),
});createDb is cheap to call per request. The returned Database type is what other packages accept when they need database access.
Tables
src/schema.ts is the single source of truth. It defines:
- Better Auth:
user,session,account,verification,deviceCode,apikey - Organizations:
organization,member,invitation - Notifications:
notification,notificationPreference,notificationSettings
Schema changes
Edit private/db/src/schema.ts, then push with drizzle-kit:
cd private/db
pnpm push # local sqlite created by `vp dev`
pnpm push:remote # production D1pnpm push:remote reads CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_D1_TOKEN from private/db/.env (gitignored, see .env.example). The token needs the Account > D1 > Edit permission.
Seeding
pnpm seed force-pushes the schema and writes the deterministic seed data. Tests import the same constants:
import { seedOrganization, seedUser } from '@onyx/db/seed-data';