Skip to content

feat: add database package with Drizzle and Better Auth schema - #1

Merged
quality-runtime[bot] merged 1 commit into
mainfrom
feat/database-package
Sep 18, 2026
Merged

quality-runtime[bot] merged 1 commit into
mainfrom
feat/database-package

Conversation

@quality-runtime

Copy link
Copy Markdown
Contributor

Adds packages/db — the Drizzle schema, client, and migration history — with authentication and tenancy owned by Better Auth rather than reimplemented. Organizations are the tenant boundary (TENANT-01), so they exist from the first migration instead of being retrofitted around later.

Decisions are recorded in ADR 0001 (why Drizzle + Better Auth, which plugins, what the tests do and do not prove) and ADR 0002 (identifier format). docs/data-model.md describes the resulting model; docs/security.md gains the tenant-authorization guardrail.

Schema

Eight tables from Better Auth's organization, admin, and twoFactor plugins. Teams and dynamic access control are off — both add tables and neither has a requirement yet.

The Drizzle schema is maintained by hand rather than generated into the tree, because generated files stop being reviewed. Four deviations deliberately strengthen what Better Auth generates:

Deviation Why
timestamptz throughout Audit history must be unambiguous across zones
UNIQUE (organization_id, user_id) Better Auth checks before inserting, which races
UNIQUE (provider_id, account_id) Better Auth throws when this pair matches two rows, so a duplicate breaks sign-in
FK on session.active_organization_id A session must not outlive the tenant it points at

Identifiers

Prefixed, application-generated, lowercase base36 — usr_v1stgxr8z5jdhi6b. packages/db/id.ts is the only place they are generated, and each table enforces its own format in the database:

CONSTRAINT "user_id_format" CHECK ("id" ~ '^usr_[0-9a-z]{16}$')

That makes the convention an invariant rather than a habit: a writer not wired to id.ts — most importantly a Better Auth config that forgets generateId — fails on its first insert instead of quietly storing a second format.

Boundaries

createDatabase takes a pg Client or Pool, not a connection string, so connection lifecycle stays with the deployment (ARCH-01) — a long-lived pool on a server, a per-request client behind Hyperdrive on Workers:

const db = createDatabase(new Pool({ connectionString: env.DATABASE_URL }));

No Drizzle relations() are declared. They serve Better Auth's opt-in join mode and Drizzle's relational queries; neither is in use, so they are not maintained unused.

Verification

bun run check, bun run test (72 passing), and reuse lint all pass. CI now runs check and test — previously tests were not a gate, so a Better Auth upgrade could go green without executing the drift guard.

  • schema/auth.test.ts — structural check against getAuthTables(), so an upgrade that adds or renames a field fails in CI.
  • schema/migrations.test.ts — applies the checked-in SQL to real PostgreSQL via PGlite (no service container) and asserts the constraints actually reject: wrong-prefix and uppercase IDs, duplicate memberships, duplicate account identities.

Not verified: no application consumes this yet, and nothing automatically compares column types, uniqueness, indexes, or references against Better Auth's definitions — ADR 0001 records that gap and the upgrade procedure that covers it.

Add `packages/db` as the home for the schema, the Drizzle client, and the migration history, with authentication and tenancy owned by Better Auth's organization, admin, and two-factor plugins rather than reimplemented. Organizations are the tenant boundary, so they exist from the first migration instead of being retrofitted around later.
The schema is maintained by hand and guarded by tests rather than regenerated into the tree: `auth.test.ts` checks it structurally against what Better Auth reports it writes, and `migrations.test.ts` applies the checked-in SQL to PostgreSQL through PGlite and exercises the constraints it creates. CI runs both, so a Better Auth upgrade cannot pass without executing them.
Identifiers are prefixed, generated by the application, and enforced per table by a CHECK constraint. Deliberate strengthenings over Better Auth's generated schema — timestamptz, unique memberships, unique account identities, and a foreign key on the active organization — are recorded in `schema/auth.ts` and ADR 0001.
@quality-runtime
quality-runtime Bot merged commit 60d471f into main Sep 18, 2026
3 checks passed
@quality-runtime
quality-runtime Bot deleted the feat/database-package branch September 18, 2026 00:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants