schema-architect
Skill0 starsChanged 6 months agoMITView on GitHub
Design database schemas, indexes, and migration files. Covers Postgres, MySQL, SQLite, and MongoDB. Catches normalization mistakes, designs index strategy from the query patterns, writes reversible migrations with up/down halves, and flags the foreign-key + cascade choices people usually get wrong. Use when the user says "design the schema", "model this data", "create a migration", "what indexes do I need", "is this normalized", or pastes an ER sketch and asks for a real schema.
- Deletes or force-pushes
1 other project uses a version of this.
Adopted by
Most recently taken up by
- fatih-developer/fth-skillstheir own version, first seen Oct 13, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- fatih-developer/fth-skills★ 7same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository16 other files it gives its agents.
- for-codex/AGENTS.mdAGENTS.md
- api-architectSkill
- browser-qaSkill
- code-auditorSkill
- doc-craftSkill
- git-flow-proSkill
- mcp-forgeSkill
- perf-hunterSkill
- refactor-masterSkill
- resilience-engineerSkill
- security-sentinelSkill
- ship-itSkill
- test-architectSkill
- threat-modelerSkill
- type-tightenerSkill
- ui-polishSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
19 rules. About 0 are common, 11 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈11
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 18
Do *not* trigger for: ORM-only changes that don't touch the underlying schema, query optimization on existing schemas (use perf-hunter), or …
- Distinctive: line 45
If the choice is open:
- Distinctive: line 74
Foreign key lookups: every FK column gets an index automatically (Postgres doesn't add one for you).
- Very specific: line 76
Partial index: when most rows don't match (WHERE deleted_at IS NULL, WHERE status = 'open').
- Distinctive: line 119
Before declaring done, walk through:
- Distinctive: line 127
Add the new column nullable
- Very specific: line 129
Add the NOT NULL constraint
- Distinctive: line 132
Document this sequence; don't try to do it in one migration.
- Very specific: line 145
Use CITEXT for case-insensitive text columns (Postgres extension) — saves you from LOWER(email) everywhere.
- Very specific: line 146
Add CHECK constraints liberally for invariants (CHECK (start_at < end_at)).
- Very specific: line 147
Use JSONB for genuinely schemaless metadata, but index the specific keys you query (CREATE INDEX ...
- Distinctive: line 151
Don't EAV ("entity-attribute-value") for "flexibility".
- Very specific: line 152
Don't use VARCHAR(255) cargo-culted from MySQL.
- Distinctive: line 153
Don't store JSON when you have structure.
- Distinctive: line 154
Don't index foreign keys that you never join on.
- Very specific: line 155
Don't use SERIAL for new Postgres work — use GENERATED BY DEFAULT AS IDENTITY.
- Distinctive: line 161
Read: invitations belong to organizations, are sent to email addresses, expire, and are accepted to create memberships.
- Distinctive: line 164
Look up invitation by token (public accept page)
- Distinctive: line 171
Write the migration shown above.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
schema-architect — model the data so the queries get fast
When to use this skill
Trigger when the user needs schema work or migration work. Strong signals:
- "design a schema for", "model this data"
- "write a migration that adds X"
- "what indexes do I need for query Y"
- "should this be one table or two?"
- "is this normalized correctly?"
Do not trigger for: ORM-only changes that don't touch the underlying schema, query optimization on existing schemas (use perf-hunter), or for trivial column additions (just add it, match the project's migration tool).
The output contract
Schema or migration artifacts that:
- Capture the domain — table names are real nouns, columns are unambiguous.
- Are normalized to the right degree — usually 3NF, with explicit, justified denormalization where reads dominate.
- Have a deliberate index strategy — every index has a query it supports; no "let's add an index on everything".
- Are reversible — every
upmigration has adownthat gets you back to the prior state without data loss (or explicitly documents why it can't). - Run on production safely — no naive
ALTERon huge tables without a plan.
Workflow
1 — Read the domain
From the spec or the user's description, list:
- Entities (the nouns:
User,Organization,Subscription,Invoice) - Relationships (1:1, 1:many, many:many, polymorphic)
- Lifecycle events (created, soft-deleted, archived, restored)
- Queries the app will run (
find all open invoices for an org,count active users by signup month)
The queries shape the indexes. If the user can't list 5 queries, ask them.
2 — Pick the engine if it's not picked
If the choice is open:
- Postgres — default for OLTP unless there's a specific reason not to. Best-in-class indexing, JSON support, partial indexes, full-text search, triggers, materialized views.
- MySQL — fine, but Postgres has eaten its lead for new projects. Stick with MySQL only if the team's ops experience is there.
- SQLite — for single-process apps, local-first, embedded, or read-heavy with light writes.
- Mongo — only when the data is genuinely document-shaped, write patterns are append-mostly, and you can live without ACID transactions across collections. The "schemaless" pitch is a trap for relational data.
3 — Design the tables
For each entity:
- Primary key:
idasBIGINT(autoincrement) for internal-only, orTEXTstoring a ULID/UUID for anything user-facing or distributed. Never expose autoincrement IDs in URLs. - Timestamps:
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()andupdated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(). Trigger or app-level updatesupdated_at. - Soft delete:
deleted_at TIMESTAMPTZ(nullable). Index it partial:WHERE deleted_at IS NULL. - Foreign keys: real FKs with
ON DELETEaction decided per relationship:RESTRICT(default) — don't let the parent go awayCASCADE— children belong to parent and should die with it (line items of an invoice)SET NULL— children survive but lose the link (creator of a post that gets anonymized)
- Nullability: explicit.
NOT NULLwith aDEFAULTis almost always better than nullable with implicit "absent means default". - Enums:
CHECKconstraint on a text column for low-cardinality (status IN ('open','closed','archived')), or a real enum type in Postgres. Avoid MySQL ENUMs (they're painful to alter). - Money:
NUMERIC(12,2)for human currency, or integer cents (amount_cents BIGINT). NeverFLOAT. - Booleans:
BOOLEAN NOT NULL DEFAULT FALSE.
4 — Index strategy
For each query the app will run, the rule is:
- Equality filter + sort: composite index on
(filter_col, sort_col)in that order. - Range filter: index on the range column; if there's also an equality filter, equality first.
- Foreign key lookups: every FK column gets an index automatically (Postgres doesn't add one for you).
- Unique constraints: pair with the unique index they imply.
- Partial index: when most rows don't match (
WHERE deleted_at IS NULL,WHERE status = 'open'). - Covering index (Postgres
INCLUDE): when the index alone can answer the query.
For each index, write a comment explaining the query it supports. If you can't, drop the index.
5 — Write the migration
For Postgres + a typical migration tool (Knex, Prisma, sqlx, Alembic):
-- up
CREATE TABLE invitations (
id TEXT PRIMARY KEY,
org_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
email CITEXT NOT NULL,
token TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending','accepted','revoked','expired')),
invited_by BIGINT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
accepted_at TIMESTAMPTZ
);
-- one pending invitation per (org, email)
CREATE UNIQUE INDEX invitations_unique_pending
ON invitations (org_id, email)
WHERE status = 'pending';
-- token lookup for the public accept page
CREATE UNIQUE INDEX invitations_by_token ON invitations (token);
-- "list pending invitations for an org"
CREATE INDEX invitations_org_status ON invitations (org_id, status)
WHERE status = 'pending';
-- down
DROP TABLE invitations;6 — The production safety check
Before declaring done, walk through:
- Will this
ALTERlock the table? On Postgres, adding aNOT NULLcolumn with a default >= 11 is fast; without a default, it's a rewrite. - Will this index build block writes?
CREATE INDEX CONCURRENTLYfor large tables in Postgres. - Does the down migration drop data? Say so explicitly. The user might want a
downthat errors out. - Are there backfills needed? Write them as separate, idempotent steps. Long backfills shouldn't run inside a single migration transaction.
For big tables (millions of rows), the migration becomes multi-step:
- Add the new column nullable
- Backfill in batches
- Add the
NOT NULLconstraint - Switch app code
Document this sequence; don't try to do it in one migration.
7 — Handover
Output:
- The migration file(s)
- A short comment block at the top explaining the why
- The query list the indexes support
- Any caveats: backfill plan, downtime risk, irreversible operations
Patterns and anti-patterns
✅ Do:
- Use
CITEXTfor case-insensitive text columns (Postgres extension) — saves you fromLOWER(email)everywhere. - Add
CHECKconstraints liberally for invariants (CHECK (start_at < end_at)). - Use
JSONBfor genuinely schemaless metadata, but index the specific keys you query (CREATE INDEX ... ON tbl ((data->>'tenant_id'))). - Name FKs and indexes explicitly. Default names (
fk_xxx_yyy_zzz_abc) are unreadable.
❌ Don't:
- Don't EAV ("entity-attribute-value") for "flexibility". It kills query performance and type safety.
- Don't use
VARCHAR(255)cargo-culted from MySQL. In PostgresTEXTis the same, with no cost. - Don't store JSON when you have structure. JSON is for shape that genuinely varies per row.
- Don't index foreign keys that you never join on. Some FKs exist only for referential integrity.
- Don't use
SERIALfor new Postgres work — useGENERATED BY DEFAULT AS IDENTITY. SERIAL has known issues with sequence ownership.
Example invocation
User: "Design the schema for an org invitations feature. Postgres."
- Read: invitations belong to organizations, are sent to email addresses, expire, and are accepted to create memberships.
- Queries:
- List pending invitations for an org (admin view)
- Look up invitation by token (public accept page)
- Find pending invitations for a given email (when the user signs up)
- Decisions:
invitationstable with FK toorganizations(CASCADE) andusers(RESTRICT oninvited_by)- Unique partial index for
(org_id, email) WHERE status = 'pending'— prevents duplicate active invites without blocking re-invitation after revoke - Token is unique
- Status uses CHECK constraint enum
- Write the migration shown above.
- Note: token is generated app-side (use
crypto.randomBytes(32).toString('base64url')) — schema doesn't enforce format. - Caveat: if the org is hard-deleted, all pending invites cascade — that's intentional; document it in the org-deletion runbook.
See also
api-architect— design the API on top of the schemaperf-hunter— when a schema is in place but queries are slowcode-auditor— sweep the codebase after a schema change to find ORM models that drifted
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

