ZeroStarterRC
ZeroStarter

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.db to reuse connections across hot reloads
  • Docker: When INTERNAL_API_URL is set, localhost in POSTGRES_URL is automatically replaced with host.docker.internal

Schema

The schema is defined in packages/db/src/schema/auth.ts and consists of 9 tables:

Core Auth Tables

TablePurposeKey Columns
userApplication usersid, name, email (unique), emailVerified, image
sessionLogin sessionsid, token (unique), expiresAt, userId, ipAddress, userAgent, activeOrganizationId
accountOAuth provider linksid, accountId, providerId, userId, OAuth tokens
verificationEmail verification tokensid, identifier, value, expiresAt

Organization & Team Tables

TablePurposeKey Columns
organizationWorkspacesid, name, slug (unique), logo, metadata
memberUser↔Org membershipid, organizationId, userId, role (default: "member")
teamSub-groups in orgsid, name, organizationId
teamMemberUser↔Team membershipid, teamId, userId
invitationPending invitesid, 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, invitations
  • organization → many teams, members, invitations
  • team → one organization, many team members
  • session → one user

Migrations

Migrations are generated and managed with Drizzle Kit.

Creating a Migration

  1. Modify the schema in packages/db/src/schema/
  2. Generate a migration:
bun run db:generate
  1. Review the generated SQL in packages/db/drizzle/
  2. Apply the migration:
bun run db:migrate

Viewing Data

Open Drizzle Studio to browse and edit data:

bun run db:studio

Existing Migrations

MigrationDescription
0000_zero_starter.sqlCore auth tables: user, session, account, verification
0001_better_auth_organization.sqlOrganization support: organization, team, teamMember, member, invitation + session columns

Adding New Tables

  1. 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(),
})
  1. Export from packages/db/src/schema/index.ts
  2. Run bun run db:generate then bun 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:

  • Neon — Serverless Postgres
  • Supabase — Postgres with real-time
  • Railway — Managed databases

For local development, you can create a database instantly with:

bunx pglaunch -k