Database
Database Overview
Type-safe database access with Drizzle ORM
The @repo/db package provides type-safe database access using Drizzle ORM with PostgreSQL.
Architecture
StartupKit uses a local package architecture. Your project imports from @repo/db (a local workspace package), not directly from external packages:
your-project/
├── packages/
│ └── db/ # @repo/db - local package
│ ├── src/
│ │ ├── index.ts # Database client
│ │ └── schema.ts # Your schema
│ └── drizzle/ # Migrations
└── apps/
└── web/ # Imports from @repo/dbThis architecture means:
- Full control - Modify the database layer to fit your needs
- Type safety - Schema types are available throughout your app
- Single source of truth - One schema definition for all apps
Features
- Drizzle ORM - Type-safe queries with full TypeScript support
- PostgreSQL - Production-ready relational database
- Connection pooling - Optimized for serverless environments
- Migrations - Version-controlled schema changes
- Drizzle Studio - Visual database browser
Quick Start
Import the client
import { db, users } from "@repo/db"
import { eq } from "drizzle-orm"
export async function GET() {
const allUsers = await db.select().from(users)
return Response.json(allUsers)
}Query with relations
import { db, users, teams, teamMembers } from "@repo/db"
import { eq } from "drizzle-orm"
const userWithTeams = await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
memberships: {
with: {
team: true
}
}
}
})Environment Setup
Add your database connection string to .env.local:
DATABASE_URL="postgresql://user:password@host:5432/database"For local development, you can use:
- Neon - Serverless Postgres (recommended)
- Supabase - Postgres with extras
- Local PostgreSQL via Docker
Available Scripts
Run these from the packages/db directory:
| Command | Description |
|---|---|
pnpm db:generate | Generate migrations from schema changes |
pnpm db:migrate | Apply pending migrations |
pnpm db:push | Push schema directly (dev only) |
pnpm db:studio | Open Drizzle Studio |
Included Schema
The starter includes tables for authentication and teams:
| Table | Purpose |
|---|---|
users | User accounts |
accounts | OAuth provider accounts |
sessions | User sessions |
teams | Team/organization records |
teamMembers | Team membership with roles |
verifications | Email verification tokens |
Type Exports
Each table exports inferred types for selects and inserts:
import type { User, NewUser, Team, NewTeam } from "@repo/db"| Type | Description |
|---|---|
User | Select type for users table |
NewUser | Insert type for users table |
Team | Select type for teams table |
NewTeam | Insert type for teams table |
TeamMember | Select type for team members |
NewTeamMember | Insert type for team members |
Account | Select type for OAuth accounts |
Session | Select type for sessions |
Verification | Select type for verifications |
Using types in your code
import { db, users, type User, type NewUser } from "@repo/db"
async function createUser(data: NewUser): Promise<User> {
const [user] = await db.insert(users).values(data).returning()
return user
}
async function getUserById(id: string): Promise<User | undefined> {
return db.query.users.findFirst({
where: (users, { eq }) => eq(users.id, id)
})
}Extending the schema
When you add new tables, export types using Drizzle's inference helpers:
import { pgTable, text, timestamp } from "drizzle-orm/pg-core"
export const posts = pgTable("Post", {
id: text("id").primaryKey(),
title: text("title").notNull(),
content: text("content"),
authorId: text("authorId").notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull()
})
export type Post = typeof posts.$inferSelect
export type NewPost = typeof posts.$inferInsertNext Steps
- Schema definition - Define your data models
- Migrations - Manage schema changes
- Queries - Query patterns and examples