Database
PostgreSQL database schema, Drizzle ORM configuration, and migration workflows.
Overview
ZeroStarter uses PostgreSQL with Drizzle ORM for type-safe database operations. The database package lives at packages/db/ and provides a shared database client and schema used by the auth and API packages.
Connection
The database client is initialized in packages/db/src/index.ts using Bun's native SQL driver:
import { env } from "@packages/env/db"
import { drizzle } from "drizzle-orm/bun-sql"
import { SQL } from "bun"
const client = new SQL({
url: env.POSTGRES_URL,
tls: isProduction() ? { rejectUnauthorized: true } : false,
connectionTimeout: 10,
idleTimeout: 30,
maxLifetime: 0,
})
export const db = drizzle({ client, schema })Connection Pooling
- Production: New client per import, TLS enabled with certificate verification
- Non-production: Singleton pattern via
globalThis.dbto reuse connections across hot reloads - Docker: When
INTERNAL_API_URLis set,localhostinPOSTGRES_URLis automatically replaced withhost.docker.internal
Schema
The schema is defined in packages/db/src/schema/auth.ts and consists of 9 tables:
Core Auth Tables
| Table | Purpose | Key Columns |
|---|---|---|
user | Application users | id, name, email (unique), emailVerified, image |
session | Login sessions | id, token (unique), expiresAt, userId, ipAddress, userAgent, activeOrganizationId |
account | OAuth provider links | id, accountId, providerId, userId, OAuth tokens |
verification | Email verification tokens | id, identifier, value, expiresAt |
Organization & Team Tables
| Table | Purpose | Key Columns |
|---|---|---|
organization | Workspaces | id, name, slug (unique), logo, metadata |
member | User↔Org membership | id, organizationId, userId, role (default: "member") |
team | Sub-groups in orgs | id, name, organizationId |
teamMember | User↔Team membership | id, teamId, userId |
invitation | Pending invites | id, organizationId, email, role, status, expiresAt, inviterId |
All foreign keys use CASCADE on delete. Tables include appropriate indexes on foreign key columns.
Relations
Drizzle relations are defined for type-safe query joins:
user→ many sessions, accounts, team memberships, org memberships, invitationsorganization→ many teams, members, invitationsteam→ one organization, many team memberssession→ one user
Migrations
Migrations are generated and managed with Drizzle Kit.
Creating a Migration
- Modify the schema in
packages/db/src/schema/ - Generate a migration:
bun run db:generate- Review the generated SQL in
packages/db/drizzle/ - Apply the migration:
bun run db:migrateViewing Data
Open Drizzle Studio to browse and edit data:
bun run db:studioExisting Migrations
| Migration | Description |
|---|---|
0000_zero_starter.sql | Core auth tables: user, session, account, verification |
0001_better_auth_organization.sql | Organization support: organization, team, teamMember, member, invitation + session columns |
Adding New Tables
- Create or edit a schema file in
packages/db/src/schema/:
import { pgTable, text, timestamp } from "drizzle-orm/pg-core"
export const project = pgTable("project", {
id: text("id").primaryKey(),
name: text("name").notNull(),
organizationId: text("organization_id")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at").defaultNow().notNull(),
})- Export from
packages/db/src/schema/index.ts - Run
bun run db:generatethenbun run db:migrate
Configuration
Drizzle Kit configuration in packages/db/drizzle.config.ts:
export default defineConfig({
dialect: "postgresql",
dbCredentials: { url: env.POSTGRES_URL },
schema: "src/schema",
out: "drizzle",
})PostgreSQL Providers
For production, use a managed PostgreSQL service:
For local development, you can create a database instantly with:
bunx pglaunch -k