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
  1. Run the line below. It pulls the whole folder into ~/.claude/skills/database-schema-designer.
  2. Describe your job in plain words. Claude Code follows the skill from there.
Claude Code — installs the whole folder, not just SKILL.md
npx degit alirezarezvani/claude-skills/engineering/skills/database-schema-designer#main ~/.claude/skills/database-schema-designer

For one project only, change the path to .claude/skills/database-schema-designer.

Claude (web or desktop app)
  1. On this page open ⋯ → Download .md.
  2. Save it as SKILL.md in a folder, zip the folder, then Customize → Skills → + → Create skill → Upload a skill.
  3. Pick the file and Save. Claude shows the name and description and runs a security scan.
  4. Check the skill is switched on.
  5. Start a new chat and describe your job in plain words. The AI follows the skill from there.
ChatGPT or another app
  1. ChatGPT: make a Project and paste it into Instructions.
  2. 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.
Step-by-step guide with screenshots · Ask in the forum

Paste into Claude, ChatGPT or Cursor.

Source of Database schema designer

Show the full text249 lines
namedescription
database-schema-designerUse 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_id to all tenant-scoped tables
  • Soft deletes: add deleted_at TIMESTAMPTZ instead of hard deletes
  • Audit trail: add created_by, updated_by, created_at, updated_at
  • Versioning: add version INTEGER for 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 NULL without 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 version column
  • RLS not tested — always test RLS with a non-superuser role

Best Practices

  1. Timestamps everywhere — created_at, updated_at on every table
  2. Soft deletes for auditable data — deleted_at instead of DELETE
  3. Audit log for compliance — log before/after JSON for regulated domains
  4. UUIDs or CUIDs as PKs — avoid sequential integer leakage
  5. Index foreign keys — every FK column should have an index
  6. Partial indexes — use WHERE deleted_at IS NULL for active-only queries
  7. RLS over application-level filtering — database enforces tenancy, not just app code
1---
2name: "database-schema-designer"
3description: "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 
16Design 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 
44Given 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 
47Extract entities:
48```
49User, Project, Task, Label, TaskLabel (junction), TaskAssignment, AuditLog
50```
51 
52### Step 2: Identify Relationships
53 
54```
55User 1──* Project (owner)
56Project 1──* Task
57Task *──* Label (via TaskLabel)
58Task *──* User (via TaskAssignment)
59User 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```sql
77-- Enable RLS
78ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
79ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
80 
81-- Create app role
82CREATE ROLE app_user;
83 
84-- Users can only see tasks in their organization's projects
85CREATE 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
96CREATE 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
101CREATE 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)
115SELECT set_config('app.current_user_id', $1, true);
116```
117 
118---
119 
120## Seed Data Generation
121 
122```typescript
123// db/seed.ts
124import { faker } from '@faker-js/faker'
125import { db } from './client'
126import { organizations, users, projects, tasks } from './schema'
127import { createId } from '@paralleldrive/cuid2'
128import { hashPassword } from '../src/lib/auth'
129 
130async 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 
181seed().catch(console.error).finally(() => process.exit(0))
182```
183 
184---
185 
186## ERD Generation (Mermaid)
187 
188```
189erDiagram
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 
221Generate from Prisma:
222```bash
223npx 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 
2421. **Timestamps everywhere** — `created_at`, `updated_at` on every table
2432. **Soft deletes for auditable data** — `deleted_at` instead of DELETE
2443. **Audit log for compliance** — log before/after JSON for regulated domains
2454. **UUIDs or CUIDs as PKs** — avoid sequential integer leakage
2465. **Index foreign keys** — every FK column should have an index
2476. **Partial indexes** — use `WHERE deleted_at IS NULL` for active-only queries
2487. **RLS over application-level filtering** — database enforces tenancy, not just app code
249 

Discussion