plugins/systems-design/skills/data-modeling/SKILL.md
Designs the data layer — schemas, entities, relationships, ID strategy, naming, tenancy, timestamps, indexes, audit logging, and migrations. Turns a set of domain concepts into a concrete Drizzle schema with all the small decisions that compound into either a clean data layer or a mess. Use when the user asks to design a schema, model the data, plan the database, add tables, review an existing schema, or says things like "what should the database look like", "design the schema for X", "add a table for Y", or "how should I store Z". Does NOT cover system decomposition (use architecture) or API design (use api-design).
npx skillsauth add lucasilverentand/skills data-modelingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Turns a set of entities and relationships into a concrete schema — IDs, naming, tenancy, timestamps, indexes, migrations, audit logging. These decisions seem small individually but they compound. Get them right once, apply them consistently, and the data layer stays clean as the system grows. Get them wrong and every query, every migration, every debugging session pays the tax.
ls packages/db/schema/ 2>/dev/null || echo "(no schema directory)"ls .context/architecture/ 2>/dev/null || echo "(no architecture docs)"architecture, come back here for the data layerapi-designPick the database before modeling the schema — the choice shapes what's possible.
|Factor|D1 (SQLite on CF)|Neon (Postgres)|
|---|---|---|
|Best for|Small projects, prototypes, single-tenant tools, <100K rows|Multi-tenant SaaS, complex queries, compliance-sensitive data|
|Queries|Simple CRUD, basic JOINs, no CTEs in older versions|Full SQL: CTEs, window functions, JSON operators, full-text search|
|Tenancy|App-layer WHERE tenant_id = ? only — no RLS|Row-Level Security as defense-in-depth backstop|
|Compliance|Data in CF's nearest region (unpredictable)|EU-primary region, column encryption, GDPR-friendly|
|Scale|10GB max per DB, limited concurrent writers|Autoscaling compute, read replicas, no hard row limits|
|Cost|Extremely cheap, included in Workers plan|Per-compute-second, ~$0.10/hr active, scales to zero|
|Migration path|D1 → Neon via Drizzle (same schema, different driver)|Already at the ceiling|
|Ecosystem|SQLite dialect quirks (no ALTER COLUMN, limited types)|Full Postgres ecosystem: extensions, pg_dump, observability tools|
Decision rule: Start with D1 when the project is small, single-purpose, and internal (e.g., a team tool with <500 users, simple CRUD, no multi-tenant isolation requirements). Graduate to Neon when you need RLS, complex queries, GDPR data residency, or the data will grow past what SQLite handles comfortably. The Drizzle schema is portable — switching is a driver change plus a migration, not a rewrite.
When in doubt: ask how many tenants, whether RLS matters, and whether EU data residency is required. If any answer is "yes", pick Neon.
Use AskUserQuestion for the ones that aren't obvious from context:
tenant_id; global tables (plans, feature flags, system config) don't.Prefixed ULIDs: ord_01HF2M3N4P5QRSTVWXYZ123456.
ord_, usr_, inv_. Visible in logs, greppable, a human can tell what kind of thing they're looking at without context.text, not uuid type — because prefixed IDs aren't valid UUIDs and Postgres will reject them.orders, line_items). Because SQL is a set language — a table is a collection.created_at, tenant_id). Because Postgres folds unquoted identifiers to lowercase anyway.idx_<table>_<columns> (idx_orders_customer_id). Because you'll read index names in EXPLAIN output and slow query logs.Every table gets these unless there's a specific reason not to:
created_at timestamptz not null default now() — when the row was inserted. Immutable.updated_at timestamptz not null default now() — last modification. Application or trigger updates this on every write.deleted_at timestamptz — soft delete. null means active. Drizzle query filters exclude deleted rows by default; .withDeleted() to opt in. Because hard deletes break foreign keys, audit trails, and "undo".tenant_id text not null on every tenant-scoped table. Because tenant isolation is the single most important invariant in a multi-tenant system.WHERE tenant_id = ? to every query. Drizzle middleware or a helper function — never rely on remembering to add it per query.tenant_id first in composite indexes for tenant-scoped queries. idx_orders_tenant_customer(tenant_id, customer_id) not idx_orders_customer_tenant(customer_id, tenant_id). Because the planner uses leftmost prefix matching.When to use JSONB and when not to:
metadata jsonb, Zod-validated at write time). Because the schema for these fields is defined by the user, not the developer.Three patterns — pick the right one for the entity:
audit_log table (append-only): default for all mutations. Middleware inserts { entity, entity_id, action, actor_id, changes, timestamp } automatically. Because you always want to know who changed what and when._history tables: only for high-value entities (money, legal, compliance). A trigger snapshots the old row before update/delete. Because regulatory requirements demand a complete before/after record, not just a diff.packages/db/ workspace with schema/<domain>.ts files and a barrel index.ts. Because one package owns the schema, and every app/service imports from @repo/db.schema/orders.ts, schema/customers.ts, schema/auth.ts.packages/db/migrations/. Because migration files are SQL artifacts that belong next to the schema that generates them.drizzle-kit generate → commit SQL files → drizzle migrate in CI before deploy. Because migrations are code — they go through review and version control like everything else.Write schema definitions to:
.context/architecture/ as design documentationpackages/db/schema/ as Drizzle schema code (when the project has a db package)|When|Use|
|---|---|
|Need the system architecture first|architecture|
|Data store choice deserves an ADR|documentation:write-adr if installed|
|Designing the API over this data|api-design|
|Documenting the whole design|documentation:write-design-doc if installed|
|File|Covers|
|---|---|
|references/ids.md|Prefixed ULID strategy|
|references/naming.md|Table, column, index naming conventions|
|references/soft-delete.md|Timestamps and soft delete pattern|
|references/tenancy.md|Multi-tenancy with tenant_id + RLS|
|references/jsonb.md|When to use JSONB and when not to|
|references/audit.md|Three audit logging patterns|
|references/schema-organization.md|packages/db layout and domain files|
|references/migrations.md|Migration workflow and expand-contract|
|references/schema-examples.md|Full example schemas showing conventions applied|
tools
Creates, audits, and updates public open-source repository documentation, including README files, CONTRIBUTING guides, SECURITY and SUPPORT docs, project badges, quickstarts, usage guidance, community links, and contributor onboarding. Use when maintaining docs for public GitHub projects, libraries, CLIs, apps, or reusable packages, especially when the user says "update this README", "write CONTRIBUTING.md", "make these docs open-source ready", or "improve the public project docs".
development
Creates, audits, and updates private or closed-source project documentation, including internal README hubs, codebase navigation guides, ownership links, Linear initiative links, onboarding notes, runbooks, and contribution guidance for teams. Use when maintaining docs for private repositories, internal apps, services, infrastructure, or company projects, especially when the user says "make this README an internal hub", "document how to navigate this repo", "add Linear links to the docs", or "write private project documentation".
development
Creates, updates, estimates, and tidies Linear issues using Luca's issue-shaping rules. Use when the user asks to create a Linear issue, write ticket-ready issue text, refine an existing issue, add acceptance criteria, set issue relationships, estimate points, audit issue hygiene, tidy a Linear project, find duplicates, fix stale blockers, or normalize labels, milestones, priorities, and issue state.
testing
Keeps an existing Linear project tidy after planning and during execution. Use when the user asks to "tidy Linear", "clean up the project", "audit issues", "find duplicates", "check stale blockers", "fix project drift", or run periodic Linear housekeeping on a project, initiative, or milestone set. Use when planning is underway or execution has started and relationships, labels, priorities, documents, and issue states need coherence without changing product scope.