Docs / Database / Schema overview
Schema overview
Keel uses Drizzle ORM over Postgres, connected via postgres-js (a standard DATABASE_URL, not a provider-specific HTTP driver), so it works unchanged against Neon, Supabase, Railway, or a local Postgres instance.
File organization
Schema files live under lib/db/schema/*:
auth-schema.ts, Better Auth's own tables (user,session,account,verification). Generated bynpx @better-auth/cli generate, never hand-edited, see Sessions & middleware.- Everything else (
team.ts,billing.ts,credits.ts,api-keys.ts,notifications.ts,feature-flags.ts,app-settings.ts,audit-log.ts,relations.ts), Keel's own domain tables, re-exported throughapp.ts. index.tsre-exports bothapp.tsandauth-schema.ts, the single import surface the rest of the app uses (import { teams, credits } from "@/lib/db/schema").columns.tsholds three shared column builders used across every custom table:id()(a UUID primary key formatted to match Better Auth's own id shape, so joins between the two stay consistent),createdAt(), andupdatedAt()(auto-touches on every update via$onUpdate).
The tables
Teams, billing, and AI credits: teams, team_members, invites, subscriptions, credit_purchases, checkout_locks, credits, usage_events. Everything else: api_keys, notifications, feature_flags, app_settings, audit_log.
Every custom table carries a team_id
Teams/RBAC are core v1, not a bolt-on, so every table above (including credits and usage_events, not just the obviously "team" ones) has a team_id foreign key from its first migration. See Team model for the full rule and why it's enforced through lib/dal/*.ts rather than left to callers.
Adding a table
Add the Drizzle table definition to a new or existing file under lib/db/schema/, re-export it from app.ts if it's a new file, then generate and apply a migration, see Migrations. Read the table through a lib/dal/*.ts function, not an inline query in a route handler, matching every existing table.