Schema architect
Skill ak-ship/fullstack-agent-skills/skills/schema-architect
15 production-grade Claude Code skills that turn it into a full-stack engineering agent — design, code, test, secure, ship. Also works with OpenAI Codex CLI. MIT.
npx -y skills add ak-ship/fullstack-agent-skills --skill schema-architectAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
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.
SKILL.md
9.1 KB, ~2.1k tokens by cl100k_base, as published. Nobody here has run it
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
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.