Database schema designer
Use when the user asks to create ERD diagrams, normalize database schemas, design table relationships, or plan schema migrations.
How to use it
Claude Code
- Run the line below. It pulls the whole folder into
~/.claude/skills/database-schema-designer. - Describe your job in plain words. Claude Code follows the skill from there.
npx degit alirezarezvani/claude-skills/engineering/skills/database-schema-designer#main ~/.claude/skills/database-schema-designerFor one project only, change the path to .claude/skills/database-schema-designer.
Claude (web or desktop app)
- On this page open ⋯ → Download .md.
- Save it as SKILL.md in a folder, zip the folder, then Customize → Skills → + → Create skill → Upload a skill.
- Pick the file and Save. Claude shows the name and description and runs a security scan.
- Check the skill is switched on.
- Start a new chat and describe your job in plain words. The AI follows the skill from there.
ChatGPT or another app
- ChatGPT: make a Project and paste it into Instructions.
- Neither? Paste it at the top of a new chat — it works for that chat.
Not working?
- Check which app you pasted it into — the steps above name the right one.
- Some skills need the paid tier of Claude or ChatGPT.
Paste into Claude, ChatGPT or Cursor.
Source of Database schema designer
Show the full text249 lines
| name | description |
|---|---|
| database-schema-designer | Use when the user asks to create ERD diagrams, normalize database schemas, design table relationships, or plan schema migrations. |
Database Schema Designer
Tier: POWERFUL
Category: Engineering
Domain: Data Architecture / Backend
Overview
Design relational database schemas from requirements and generate migrations, TypeScript/Python types, seed data, RLS policies, and indexes. Handles multi-tenancy, soft deletes, audit trails, versioning, and polymorphic associations.
Core Capabilities
- Schema design — normalize requirements into tables, relationships, constraints
- Migration generation — Drizzle, Prisma, TypeORM, Alembic
- Type generation — TypeScript interfaces, Python dataclasses/Pydantic models
- RLS policies — Row-Level Security for multi-tenant apps
- Index strategy — composite indexes, partial indexes, covering indexes
- Seed data — realistic test data generation
- ERD generation — Mermaid diagram from schema
When to Use
- Designing a new feature that needs database tables
- Reviewing a schema for performance or normalization issues
- Adding multi-tenancy to an existing schema
- Generating TypeScript types from a Prisma schema
- Planning a schema migration for a breaking change
Schema Design Process
Step 1: Requirements → Entities
Given requirements:
"Users can create projects. Each project has tasks. Tasks can have labels. Tasks can be assigned to users. We need a full audit trail."
Extract entities:
User, Project, Task, Label, TaskLabel (junction), TaskAssignment, AuditLog
Step 2: Identify Relationships
User 1──* Project (owner)
Project 1──* Task
Task *──* Label (via TaskLabel)
Task *──* User (via TaskAssignment)
User 1──* AuditLog
Step 3: Add Cross-cutting Concerns
- Multi-tenancy: add
organization_idto all tenant-scoped tables - Soft deletes: add
deleted_at TIMESTAMPTZinstead of hard deletes - Audit trail: add
created_by,updated_by,created_at,updated_at - Versioning: add
version INTEGERfor optimistic locking
Full Schema Example (Task Management SaaS)
→ See references/full-schema-examples.md for details
Row-Level Security (RLS) Policies
-- Enable RLS
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Create app role
CREATE ROLE app_user;
-- Users can only see tasks in their organization's projects
CREATE POLICY tasks_org_isolation ON tasks
FOR ALL TO app_user
USING (
project_id IN (
SELECT p.id FROM projects p
JOIN organization_members om ON om.organization_id = p.organization_id
WHERE om.user_id = current_setting('app.current_user_id')::text
)
);
-- Soft delete: never show deleted records
CREATE POLICY tasks_no_deleted ON tasks
FOR SELECT TO app_user
USING (deleted_at IS NULL);
-- Only task creator or admin can delete
CREATE POLICY tasks_delete_policy ON tasks
FOR DELETE TO app_user
USING (
created_by_id = current_setting('app.current_user_id')::text
OR EXISTS (
SELECT 1 FROM organization_members om
JOIN projects p ON p.organization_id = om.organization_id
WHERE p.id = tasks.project_id
AND om.user_id = current_setting('app.current_user_id')::text
AND om.role IN ('owner', 'admin')
)
);
-- Set user context (call at start of each request)
SELECT set_config('app.current_user_id', $1, true);
Seed Data Generation
// db/seed.ts
import { faker } from '@faker-js/faker'
import { db } from './client'
import { organizations, users, projects, tasks } from './schema'
import { createId } from '@paralleldrive/cuid2'
import { hashPassword } from '../src/lib/auth'
async function seed() {
console.log('Seeding database...')
// Create org
const [org] = await db.insert(organizations).values({
id: createId(),
name: "acme-corp",
slug: 'acme',
plan: 'growth',
}).returning()
// Create users
const adminUser = await db.insert(users).values({
id: createId(),
email: '[email protected]',
name: "alice-admin",
passwordHash: await hashPassword('password123'),
}).returning().then(r => r[0])
// Create projects
const projectsData = Array.from({ length: 3 }, () => ({
id: createId(),
organizationId: org.id,
ownerId: adminUser.id,
name: "fakercompanycatchphrase"
description: faker.lorem.paragraph(),
status: 'active' as const,
}))
const createdProjects = await db.insert(projects).values(projectsData).returning()
// Create tasks for each project
for (const project of createdProjects) {
const tasksData = Array.from({ length: faker.number.int({ min: 5, max: 20 }) }, (_, i) => ({
id: createId(),
projectId: project.id,
title: faker.hacker.phrase(),
description: faker.lorem.sentences(2),
status: faker.helpers.arrayElement(['todo', 'in_progress', 'done'] as const),
priority: faker.helpers.arrayElement(['low', 'medium', 'high'] as const),
position: i * 1000,
createdById: adminUser.id,
updatedById: adminUser.id,
}))
await db.insert(tasks).values(tasksData)
}
console.log(`✅ Seeded: 1 org, ${projectsData.length} projects, tasks`)
}
seed().catch(console.error).finally(() => process.exit(0))
ERD Generation (Mermaid)
erDiagram
Organization ||--o{ OrganizationMember : has
Organization ||--o{ Project : owns
User ||--o{ OrganizationMember : joins
User ||--o{ Task : "created by"
Project ||--o{ Task : contains
Task ||--o{ TaskAssignment : has
Task ||--o{ TaskLabel : has
Task ||--o{ Comment : has
Task ||--o{ Attachment : has
Label ||--o{ TaskLabel : "applied to"
User ||--o{ TaskAssignment : assigned
Organization {
string id PK
string name
string slug
string plan
}
Task {
string id PK
string project_id FK
string title
string status
string priority
timestamp due_date
timestamp deleted_at
int version
}
Generate from Prisma:
npx prisma-erd-generator
# or: npx @dbml/cli prisma2dbml -i schema.prisma | npx dbml-to-mermaid
Common Pitfalls
- Soft delete without index —
WHERE deleted_at IS NULLwithout index = full scan - Missing composite indexes —
WHERE org_id = ? AND status = ?needs a composite index - Mutable surrogate keys — never use email or slug as PK; use UUID/CUID
- Non-nullable without default — adding a NOT NULL column to existing table requires default or migration plan
- No optimistic locking — concurrent updates overwrite each other; add
versioncolumn - RLS not tested — always test RLS with a non-superuser role
Best Practices
- Timestamps everywhere —
created_at,updated_aton every table - Soft deletes for auditable data —
deleted_atinstead of DELETE - Audit log for compliance — log before/after JSON for regulated domains
- UUIDs or CUIDs as PKs — avoid sequential integer leakage
- Index foreign keys — every FK column should have an index
- Partial indexes — use
WHERE deleted_at IS NULLfor active-only queries - RLS over application-level filtering — database enforces tenancy, not just app code
| 1 | |
| 2 | name "database-schema-designer" |
| 3 | description "Use when the user asks to create ERD diagrams, normalize database schemas, design table relationships, or plan schema migrations." |
| 4 | |
| 5 | |
| 6 | # Database Schema Designer |
| 7 | |
| 8 | **Tier:** POWERFUL |
| 9 | **Category:** Engineering |
| 10 | **Domain:** Data Architecture / Backend |
| 11 | |
| 12 | |
| 13 | |
| 14 | ## Overview |
| 15 | |
| 16 | Design relational database schemas from requirements and generate migrations, TypeScript/Python types, seed data, RLS policies, and indexes. Handles multi-tenancy, soft deletes, audit trails, versioning, and polymorphic associations. |
| 17 | |
| 18 | ## Core Capabilities |
| 19 | |
| 20 | **Schema design** — normalize requirements into tables, relationships, constraints |
| 21 | **Migration generation** — Drizzle, Prisma, TypeORM, Alembic |
| 22 | **Type generation** — TypeScript interfaces, Python dataclasses/Pydantic models |
| 23 | **RLS policies** — Row-Level Security for multi-tenant apps |
| 24 | **Index strategy** — composite indexes, partial indexes, covering indexes |
| 25 | **Seed data** — realistic test data generation |
| 26 | **ERD generation** — Mermaid diagram from schema |
| 27 | |
| 28 | |
| 29 | |
| 30 | ## When to Use |
| 31 | |
| 32 | Designing a new feature that needs database tables |
| 33 | Reviewing a schema for performance or normalization issues |
| 34 | Adding multi-tenancy to an existing schema |
| 35 | Generating TypeScript types from a Prisma schema |
| 36 | Planning a schema migration for a breaking change |
| 37 | |
| 38 | |
| 39 | |
| 40 | ## Schema Design Process |
| 41 | |
| 42 | ### Step 1: Requirements → Entities |
| 43 | |
| 44 | Given requirements: |
| 45 | > "Users can create projects. Each project has tasks. Tasks can have labels. Tasks can be assigned to users. We need a full audit trail." |
| 46 | |
| 47 | Extract entities: |
| 48 | |
| 49 | User, Project, Task, Label, TaskLabel (junction), TaskAssignment, AuditLog |
| 50 | |
| 51 | |
| 52 | ### Step 2: Identify Relationships |
| 53 | |
| 54 | |
| 55 | User 1──* Project (owner) |
| 56 | Project 1──* Task |
| 57 | Task *──* Label (via TaskLabel) |
| 58 | Task *──* User (via TaskAssignment) |
| 59 | User 1──* AuditLog |
| 60 | |
| 61 | |
| 62 | ### Step 3: Add Cross-cutting Concerns |
| 63 | |
| 64 | Multi-tenancy: add `organization_id` to all tenant-scoped tables |
| 65 | Soft deletes: add `deleted_at TIMESTAMPTZ` instead of hard deletes |
| 66 | Audit trail: add `created_by`, `updated_by`, `created_at`, `updated_at` |
| 67 | Versioning: add `version INTEGER` for optimistic locking |
| 68 | |
| 69 | |
| 70 | |
| 71 | ## Full Schema Example (Task Management SaaS) |
| 72 | → See references/full-schema-examples.md for details |
| 73 | |
| 74 | ## Row-Level Security (RLS) Policies |
| 75 | |
| 76 | |
| 77 | -- Enable RLS |
| 78 | ALTER TABLE tasks ENABLE ROW LEVEL SECURITY; |
| 79 | ALTER TABLE projects ENABLE ROW LEVEL SECURITY; |
| 80 | |
| 81 | -- Create app role |
| 82 | CREATE ROLE app_user; |
| 83 | |
| 84 | -- Users can only see tasks in their organization's projects |
| 85 | CREATE POLICY tasks_org_isolation ON tasks |
| 86 | FOR ALL TO app_user |
| 87 | USING ( |
| 88 | project_id IN ( |
| 89 | SELECT p.id FROM projects p |
| 90 | JOIN organization_members om ON om.organization_id = p.organization_id |
| 91 | WHERE om.user_id = current_setting('app.current_user_id')::text |
| 92 | ) |
| 93 | ); |
| 94 | |
| 95 | -- Soft delete: never show deleted records |
| 96 | CREATE POLICY tasks_no_deleted ON tasks |
| 97 | FOR SELECT TO app_user |
| 98 | USING (deleted_at IS NULL); |
| 99 | |
| 100 | -- Only task creator or admin can delete |
| 101 | CREATE POLICY tasks_delete_policy ON tasks |
| 102 | FOR DELETE TO app_user |
| 103 | USING ( |
| 104 | created_by_id = current_setting('app.current_user_id')::text |
| 105 | OR EXISTS ( |
| 106 | SELECT 1 FROM organization_members om |
| 107 | JOIN projects p ON p.organization_id = om.organization_id |
| 108 | WHERE p.id = tasks.project_id |
| 109 | AND om.user_id = current_setting('app.current_user_id')::text |
| 110 | AND om.role IN ('owner', 'admin') |
| 111 | ) |
| 112 | ); |
| 113 | |
| 114 | -- Set user context (call at start of each request) |
| 115 | SELECT set_config('app.current_user_id', $1, true); |
| 116 | |
| 117 | |
| 118 | |
| 119 | |
| 120 | ## Seed Data Generation |
| 121 | |
| 122 | |
| 123 | // db/seed.ts |
| 124 | import { faker } from '@faker-js/faker' |
| 125 | import { db } from './client' |
| 126 | import { organizations, users, projects, tasks } from './schema' |
| 127 | import { createId } from '@paralleldrive/cuid2' |
| 128 | import { hashPassword } from '../src/lib/auth' |
| 129 | |
| 130 | async function seed() { |
| 131 | console.log('Seeding database...') |
| 132 | |
| 133 | // Create org |
| 134 | const [org] = await db.insert(organizations).values({ |
| 135 | id: createId(), |
| 136 | name: "acme-corp", |
| 137 | slug: 'acme', |
| 138 | plan: 'growth', |
| 139 | }).returning() |
| 140 | |
| 141 | // Create users |
| 142 | const adminUser = await db.insert(users).values({ |
| 143 | id: createId(), |
| 144 | email: '[email protected]', |
| 145 | name: "alice-admin", |
| 146 | passwordHash: await hashPassword('password123'), |
| 147 | }).returning().then(r => r[0]) |
| 148 | |
| 149 | // Create projects |
| 150 | const projectsData = Array.from({ length: 3 }, () => ({ |
| 151 | id: createId(), |
| 152 | organizationId: org.id, |
| 153 | ownerId: adminUser.id, |
| 154 | name: "fakercompanycatchphrase" |
| 155 | description: faker.lorem.paragraph(), |
| 156 | status: 'active' as const, |
| 157 | })) |
| 158 | |
| 159 | const createdProjects = await db.insert(projects).values(projectsData).returning() |
| 160 | |
| 161 | // Create tasks for each project |
| 162 | for (const project of createdProjects) { |
| 163 | const tasksData = Array.from({ length: faker.number.int({ min: 5, max: 20 }) }, (_, i) => ({ |
| 164 | id: createId(), |
| 165 | projectId: project.id, |
| 166 | title: faker.hacker.phrase(), |
| 167 | description: faker.lorem.sentences(2), |
| 168 | status: faker.helpers.arrayElement(['todo', 'in_progress', 'done'] as const), |
| 169 | priority: faker.helpers.arrayElement(['low', 'medium', 'high'] as const), |
| 170 | position: i * 1000, |
| 171 | createdById: adminUser.id, |
| 172 | updatedById: adminUser.id, |
| 173 | })) |
| 174 | |
| 175 | await db.insert(tasks).values(tasksData) |
| 176 | } |
| 177 | |
| 178 | console.log(`✅ Seeded: 1 org, ${projectsData.length} projects, tasks`) |
| 179 | } |
| 180 | |
| 181 | seed().catch(console.error).finally(() => process.exit(0)) |
| 182 | |
| 183 | |
| 184 | |
| 185 | |
| 186 | ## ERD Generation (Mermaid) |
| 187 | |
| 188 | |
| 189 | erDiagram |
| 190 | Organization ||--o{ OrganizationMember : has |
| 191 | Organization ||--o{ Project : owns |
| 192 | User ||--o{ OrganizationMember : joins |
| 193 | User ||--o{ Task : "created by" |
| 194 | Project ||--o{ Task : contains |
| 195 | Task ||--o{ TaskAssignment : has |
| 196 | Task ||--o{ TaskLabel : has |
| 197 | Task ||--o{ Comment : has |
| 198 | Task ||--o{ Attachment : has |
| 199 | Label ||--o{ TaskLabel : "applied to" |
| 200 | User ||--o{ TaskAssignment : assigned |
| 201 | |
| 202 | Organization { |
| 203 | string id PK |
| 204 | string name |
| 205 | string slug |
| 206 | string plan |
| 207 | } |
| 208 | |
| 209 | Task { |
| 210 | string id PK |
| 211 | string project_id FK |
| 212 | string title |
| 213 | string status |
| 214 | string priority |
| 215 | timestamp due_date |
| 216 | timestamp deleted_at |
| 217 | int version |
| 218 | } |
| 219 | |
| 220 | |
| 221 | Generate from Prisma: |
| 222 | |
| 223 | npx prisma-erd-generator |
| 224 | # or: npx @dbml/cli prisma2dbml -i schema.prisma | npx dbml-to-mermaid |
| 225 | |
| 226 | |
| 227 | |
| 228 | |
| 229 | ## Common Pitfalls |
| 230 | |
| 231 | **Soft delete without index** — `WHERE deleted_at IS NULL` without index = full scan |
| 232 | **Missing composite indexes** — `WHERE org_id = ? AND status = ?` needs a composite index |
| 233 | **Mutable surrogate keys** — never use email or slug as PK; use UUID/CUID |
| 234 | **Non-nullable without default** — adding a NOT NULL column to existing table requires default or migration plan |
| 235 | **No optimistic locking** — concurrent updates overwrite each other; add `version` column |
| 236 | **RLS not tested** — always test RLS with a non-superuser role |
| 237 | |
| 238 | |
| 239 | |
| 240 | ## Best Practices |
| 241 | |
| 242 | **Timestamps everywhere** — `created_at`, `updated_at` on every table |
| 243 | **Soft deletes for auditable data** — `deleted_at` instead of DELETE |
| 244 | **Audit log for compliance** — log before/after JSON for regulated domains |
| 245 | **UUIDs or CUIDs as PKs** — avoid sequential integer leakage |
| 246 | **Index foreign keys** — every FK column should have an index |
| 247 | **Partial indexes** — use `WHERE deleted_at IS NULL` for active-only queries |
| 248 | **RLS over application-level filtering** — database enforces tenancy, not just app code |
| 249 |
Discussion
Browse more free Claude skills.