Features
Database
PostgreSQL with Drizzle ORM — schema, migrations, and conventions.
TheShipStack uses Drizzle ORM with PostgreSQL (Neon in prod, Docker locally).
Schema location
All tables are defined in db/schema/. Each file exports one or more table definitions.
db/
schema/
users.ts ← auth user table (managed by Better Auth)
profiles.ts ← user profile table
uploads.ts ← file upload records
index.ts ← re-exports all tables
index.ts ← exports the db clientAdding a table
- Create a new file in
db/schema/:
// db/schema/posts.ts
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { users } from './users'
export const posts = pgTable('posts', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
content: text('content').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
})- Re-export it from
db/schema/index.ts:
export * from './posts'- Push the schema to your database:
pnpm db:pushQuerying
Import the db client and query:
import { db } from '@/db'
import { posts } from '@/db/schema'
import { eq } from 'drizzle-orm'
const userPosts = await db.select().from(posts).where(eq(posts.userId, userId))Visualizing the database
See Local Development for Drizzle Studio and direct Postgres client connection details.
Conventions
- Never use
process.env.DATABASE_URLdirectly — it's validated and exported fromlib/env.ts - Use
uuidprimary keys withdefaultRandom() - Always add
createdAtandupdatedAttimestamps to tables - Cascade deletes from
usersto user-owned tables