Database Schema Management¶
This project uses Drizzle ORM for type-safe database migrations and schema management.
Architecture¶
backend/
├── src/
│ └── db/
│ ├── index.ts # Database connection (Drizzle + pg)
│ ├── schema/
│ │ ├── index.ts # Re-exports all tables
│ │ ├── auth.ts # Better-auth tables (user, session, account)
│ │ ├── profiles.ts # Extended user profiles
│ │ └── organizations.ts # Organization and membership tables
├── scripts/
│ ├── migrate.ts # Runtime migration runner (drizzle-orm migrator)
│ └── seed.ts # Environment-aware seed script
└── drizzle/
├── meta/ # Drizzle journal
└── migrations/ # SQL migration files
Commands¶
| Command | Description |
|---|---|
pnpm db:generate |
Generate a new SQL migration from schema changes |
pnpm db:migrate |
Apply all pending migrations (local/dev) |
pnpm db:migrate:run |
Apply migrations via runtime migrator script |
pnpm db:studio |
Open Drizzle Studio (visual DB browser) |
pnpm db:seed |
Seed the database with dev/test data |
For Dokku deployments, run migrations in-container:
Workflow¶
1. Modify the schema¶
Edit or add a table in backend/src/db/schema/:
// backend/src/db/schema/profiles.ts
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { user } from './auth';
import { organizations } from './organizations';
export const profile = pgTable('profile', {
id: uuid('id').defaultRandom().primaryKey(),
userId: text('user_id')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
role: text('role').notNull().default('teacher'),
organizationId: uuid('organization_id').references(() => organizations.id),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
2. Generate the migration¶
This creates a timestamped SQL file in drizzle/migrations/.
3. Review the migration¶
Always inspect the generated SQL before applying:
-- drizzle/migrations/0003_add_profile_table.sql
CREATE TABLE IF NOT EXISTS "profile" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"user_id" text NOT NULL REFERENCES "user"("id") ON DELETE CASCADE,
"role" text NOT NULL DEFAULT 'teacher',
"organization_id" uuid REFERENCES "organization"("id"),
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
4. Apply the migration¶
Core Tables¶
user (Better-auth managed)¶
| Column | Type | Description |
|---|---|---|
id |
text PK | Better-auth user ID |
email |
text unique | User email |
name |
text | Display name |
emailVerified |
boolean | Email verification status |
createdAt |
timestamp | Creation time |
profile¶
| Column | Type | Description |
|---|---|---|
id |
uuid PK | Profile ID |
userId |
text FK | References user.id |
role |
text | platform_admin, organization_admin, manager, coach, teacher |
organizationId |
uuid FK | References organization.id |
organization¶
| Column | Type | Description |
|---|---|---|
id |
uuid PK | Organization ID |
name |
text | Organization name |
slug |
text unique | URL-safe identifier |
createdAt |
timestamp | Creation time |
organization_user¶
| Column | Type | Description |
|---|---|---|
id |
uuid PK | Membership ID |
organizationId |
uuid FK | References organization.id |
userId |
text FK | References user.id |
role |
text | Role within the organization |
Rules¶
- Never modify the database schema manually in production.
- Always create a migration for schema changes — commit the SQL file with your PR.
- Run migrations as part of deployment (
node backend/dist/scripts/migrate.jsinside Dokku container). - Never edit a generated migration file after it has been applied to any environment.
Seeding¶
The seed script is environment-aware:
NODE_ENV=developmentseeds full test fixture data.NODE_ENV=stagingorNODE_ENV=productionrefuses to run fixture seed data.
For staging/production bootstrap, create the first platform admin via POST /api/setup/first-run.
Seed files live in backend/scripts/.