
Handles the full database design workflow from requirements gathering to production-ready schemas. It generates proper SQL or NoSQL table definitions with primary keys, foreign keys, and indexes, plus ASCII ERD diagrams and up/down migration scripts. The output follows solid conventions like snake_case naming, appropriate data types, and 3NF normalization with denormalization notes where performance matters. It covers both relational patterns (PostgreSQL, MySQL) and document stores (MongoDB) with different strategies for each. The structured format with sections for relationships, indexes, and optimization notes makes it easy to review or hand off to a team. Solid for greenfield projects or when you need to formalize an ad hoc schema that grew organically.
npx -y skills add onewave-ai/claude-skills --skill database-schema-designer --agent claude-codeInstalls into .claude/skills of the current project.
Design a schema that enforces the data's rules in the database and makes the app's real queries fast.
Collect requirements. Get from the conversation or codebase, and ask only for what blocks the design:
docker-compose.yml, ORM config, or Supabase project if present)Model entities and relationships. List entities, attributes, and cardinality (1:1, 1:N, N:M). Normalize to 3NF by default; denormalize only for a named query that needs it, and say which.
Choose keys and types using references/postgres.md (or references/other-engines.md for MySQL, SQLite, MongoDB). Defaults for Postgres: bigint generated always as identity or uuid DEFAULT uuidv7() primary keys, text over varchar(n), timestamptz for every timestamp, numeric for money.
Encode the rules as constraints. NOT NULL wherever a value is required, UNIQUE for natural keys, CHECK for ranges and allowed values, foreign keys with a deliberate ON DELETE choice for each. Data rules that live only in app code get broken by the next script or admin fix.
Derive indexes from the queries in step 1, not from a checklist. For each query, name the index that serves it. Index every foreign key column you join or cascade on (Postgres does not do this automatically). Do not add an index a UNIQUE or primary key already provides.
Write the deliverable: DDL, ERD, index rationale, and migrations with rollback, using references/schema-templates.md and the layout in references/output-format.md. Match the user's migration tool if they have one.
Verify. If a database is available (local Postgres, Docker postgres:18, a Supabase branch), run the DDL, insert a few rows, and EXPLAIN the key queries to confirm the planned indexes are used. Otherwise, walk each query from step 1 against the schema by hand. Then check the list below.
ON DELETE behavior and an index on the referencing column.varchar(255) by habit.updated_at actually updates: a DEFAULT now() only sets it on insert, so add a trigger or set it in the ORM.tenant_id (or org_id), include it in unique constraints and leading index columns, and have row-level security if the database is reachable from clients (always on Supabase).Request: "Multi-tenant project tracker: orgs, users who can belong to several orgs, projects per org, tasks per project. The main screen lists a project's open tasks by due date."
orgs, users, org_members (N:M between users and orgs with a role), projects (N:1 org), tasks (N:1 project, optional assignee).ON tasks (project_id, due_at) WHERE status <> 'done'.UNIQUE (org_id, user_id) on org_members; CHECK (role IN ('owner','admin','member')); status limited by CHECK; unique project name per org with UNIQUE (org_id, name).tasks.assignee_id to null instead of deleting tasks.tasks carries org_id so row-level security can check membership without a join through projects, and a composite foreign key (project_id, org_id) -> projects (id, org_id) keeps the two consistent.The full DDL for this example is in references/schema-templates.md.
ENUM types for values that will change. Adding a value is easy, but removing or renaming one is painful; a CHECK constraint or lookup table is easier to evolve.deleted_at) added everywhere by default. They leak into every query and unique constraint. Use them where there is a real restore or audit need, and pair them with partial unique indexes.commentable_type + commentable_id) that the database cannot enforce. Prefer separate nullable FKs with a CHECK that exactly one is set, or separate tables.jsonb. JSON is for sparse or truly variable attributes; anything you filter, join, or constrain on deserves a column.