Database Schema Designer
Tier: POWERFUL Category: Engineering / Data Architecture Maintainer: Claude Skills Team
Overview
Design normalized relational database schemas from requirements and generate migrations, TypeScript/Python types, seed data, Row-Level Security policies, index strategies, and ERD diagrams. Handles multi-tenancy, soft deletes, audit trails, optimistic locking, polymorphic associations, and temporal data patterns. Supports PostgreSQL, MySQL, and SQLite with Drizzle, Prisma, TypeORM, and Alembic.
Keywords
database schema, schema design, normalization, migration, ERD, row-level security, indexing, multi-tenancy, soft deletes, audit trail, Drizzle, Prisma, PostgreSQL
Core Capabilities
1. Schema Design from Requirements
- Extract entities and relationships from natural language requirements
- Apply normalization rules (1NF through 3NF with denormalization guidance)
- Add cross-cutting concerns: timestamps, soft deletes, audit, versioning
- Generate complete DDL with constraints, defaults, and comments
2. Migration Planning
- Generate forward and rollback migrations
- Plan zero-downtime migrations for large tables
- Handle column additions, type changes, and data backfills
- Support Drizzle, Prisma, TypeORM, Alembic, and raw SQL
3. Index Strategy
- Composite indexes for common query patterns
- Partial indexes for filtered queries (e.g., active records only)
- Covering indexes to eliminate table lookups
- GIN/GiST indexes for full-text search and JSONB
- Index bloat detection and maintenance
4. Type Generation
- TypeScript interfaces and Zod schemas from DB schema
- Python dataclasses and Pydantic models
- Enums as string unions (not database enums for migration safety)
5. Security
- Row-Level Security policies for multi-tenant isolation
- Column-level encryption for PII
- Audit logging with before/after JSON snapshots
When to Use
- Designing tables for a new feature
- Reviewing an existing schema for normalization or performance issues
- Adding multi-tenancy to a single-tenant schema
- Planning a breaking schema migration
- Generating ERD documentation for a service
Schema Design Process
Step 1: Requirements to Entities
Given requirements like:
"Users can create workspaces. Each workspace has projects. Projects contain tasks with assignees, labels, and due dates. We need audit trails and multi-tenant isolation."
Extract entities:
User, Workspace, WorkspaceMember, Project, Task, TaskAssignment,
Label, TaskLabel (junction), AuditLog
Step 2: Identify Relationships
User 1──* WorkspaceMember *──1 Workspace
Workspace 1──* Project
Project 1──* Task
Task *──* User (via TaskAssignment)
Task *──* Label (via TaskLabel)
User 1──* AuditLog
Step 3: Add Cross-Cutting Concerns
Every table gets:
id— CUID2 or UUIDv7 (sortable, non-sequential)created_at— TIMESTAMPTZ, server-side defaultupdated_at— TIMESTAMPTZ, updated on every write
Tenant-scoped tables additionally get:
workspace_id— FK to workspaces, included in every query- RLS policy enforcing workspace isolation
Auditable tables additionally get:
created_by_id— FK to usersupdated_by_id— FK to usersdeleted_at— TIMESTAMPTZ for soft deletesversion— INTEGER for optimistic locking
Step 4: Full Schema (Drizzle ORM)
import {
pgTable, text, timestamp, integer, boolean, uniqueIndex, index, pgEnum
} from 'drizzle-orm/pg-core'
import { createId } from '@paralleldrive/cuid2'
// Enums as pgEnum for type safety, but string columns also acceptable
export const taskStatusEnum = pgEnum('task_status', ['todo', 'in_progress', 'in_review', 'done'])
export const taskPriorityEnum = pgEnum('task_priority', ['low', 'medium', 'high', 'urgent'])
export const memberRoleEnum = pgEnum('member_role', ['owner', 'admin', 'member', 'viewer'])
// ──── WORKSPACES ────
export const workspaces = pgTable('workspaces', {
id: text('id').primaryKey().$defaultFn(createId),
name: text('name').notNull(),
slug: text('slug').notNull(),
plan: text('plan').notNull().default('free'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
}, (t) => [
uniqueIndex('workspaces_slug_idx').on(t.slug),
])
// ──── USERS ────
export const users = pgTable('users', {
id: text('id').primaryKey().$defaultFn(createId),
email: text('email').notNull(),
name: text('name'),
avatarUrl: text('avatar_url'),
passwordHash: text('password_hash'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
}, (t) => [
uniqueIndex('users_email_idx').on(t.email),
])
// ──── WORKSPACE MEMBERS ────
export const workspaceMembers = pgTable('workspace_members', {
id: text('id').primaryKey().$defaultFn(createId),
workspaceId: text('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }),
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
role: memberRoleEnum('role').notNull().default('member'),
joinedAt: timestamp('joined_at', { withTimezone: true }).defaultNow().notNull(),
}, (t) => [
uniqueIndex('workspace_members_unique').on(t.workspaceId, t.userId),
index('workspace_members_workspace_idx').on(t.workspaceId),
index('workspace_members_user_idx').on(t.userId),
])
// ──── PROJECTS ────
export const projects = pgTable('projects', {
id: text('id').primaryKey().$defaultFn(createId),
workspaceId: text('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
description: text('description'),
status: text('status').notNull().default('active'),
ownerId: text('owner_id').notNull().references(() => users.id),
createdById: text('created_by_id').references(() => users.id),
updatedById: text('updated_by_id').references(() => users.id),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
}, (t) => [
index('projects_workspace_idx').on(t.workspaceId),
index('projects_workspace_status_idx').on(t.workspaceId, t.status),
])
// ──── TASKS ────
export const tasks = pgTable('tasks', {
id: text('id').primaryKey().$defaultFn(createId),
projectId: text('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
description: text('description'),
status: taskStatusEnum('status').notNull().default('todo'),
priority: taskPriorityEnum('priority').notNull().default('medium'),
position: integer('position').notNull().default(0),
dueDate: timestamp('due_date', { withTimezone: true }),
version: integer('version').notNull().default(1),
createdById: text('created_by_id').notNull().references(() => users.id),
updatedById: text('updated_by_id').references(() => users.id),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
}, (t) => [
index('tasks_project_idx').on(t.projectId),
index('tasks_project_status_idx').on(t.projectId, t.status),
index('tasks_due_date_idx').on(t.dueDate).where(sql`deleted_at IS NULL`),
])
// ──── AUDIT LOG ────
export const auditLog = pgTable('audit_log', {
id: text('id').primaryKey().$defaultFn(createId),
workspaceId: text('workspace_id').notNull().references(() => workspaces.id),
userId: text('user_id').notNull().references(() => users.id),
action: text('action').notNull(), // 'create' | 'update' | 'delete'
entityType: text('entity_type').notNull(), // 'task' | 'p