Create Data Model
Overview
Design a production-ready data model from feature requirements, including entity definitions, relationships, a Mermaid ERD, index recommendations, and migration safety analysis. The output encodes senior-level database design practices: timestamps on every table, soft deletes, audit trails, and safe migration strategies.
Workflow
Read project context -- Read .chalk/docs/engineering/ for:
- Existing data model documents to understand current schema and naming conventions
- Architecture docs for database technology (PostgreSQL, MySQL, MongoDB, etc.)
- ADRs or RFCs related to data storage decisions
- If no docs exist, scan the codebase for migration files, model definitions, or schema files to infer conventions
Scan existing schema -- Use Grep to find:
- Migration files or schema definitions to understand current tables and naming patterns
- ORM model definitions (ActiveRecord, SQLAlchemy, Prisma, TypeORM, Drizzle, etc.)
- Column naming conventions (snake_case, camelCase)
- Existing timestamp patterns (
created_at/updated_at vs. createdAt/updatedAt)
- Soft delete patterns (e.g.,
deleted_at, is_deleted)
- ID strategy (auto-increment, UUID, ULID, prefixed IDs)
Determine the next document number -- List files in .chalk/docs/engineering/ matching *_data_model_*.md. Find the highest number and increment by 1.
Clarify the domain -- From $ARGUMENTS and conversation context, identify:
- The entities to model and their real-world meaning
- The relationships between entities (one-to-one, one-to-many, many-to-many)
- The key queries this model must support efficiently
- Expected data volume and growth rate (this drives index and partitioning decisions)
- Ask the user for clarification if entity boundaries are unclear
Design the entities -- For each entity, define:
- Table name (plural, snake_case -- or match project convention)
- All columns with types, constraints, and defaults
- Primary key strategy (match existing convention)
- Required standard columns (see Mandatory Columns below)
- Foreign keys with appropriate ON DELETE behavior
Design relationships -- Map all relationships:
- One-to-many: foreign key on the "many" side
- Many-to-many: explicit join table with its own timestamps and potential payload columns
- One-to-one: foreign key with unique constraint -- decide which side owns the FK
- Polymorphic associations: prefer separate FKs over
type/id pattern when possible
Create the ERD -- Generate a Mermaid ERD diagram showing all entities, their key columns, and relationships.
Recommend indexes -- Based on the expected query patterns, recommend:
- Primary key indexes (automatic)
- Foreign key indexes (always)
- Composite indexes for common query combinations
- Partial indexes for filtered queries (e.g.,
WHERE deleted_at IS NULL)
- Unique indexes for business-level uniqueness constraints
Analyze migration safety -- For each schema change, evaluate:
- Will this lock a table during migration? (ALTER TABLE on large tables)
- Is the migration reversible?
- Does it require a data backfill?
- Can it be deployed without downtime?
Write the document -- Save to .chalk/docs/engineering/<n>_data_model_<domain_slug>.md.
Confirm -- Tell the user the data model was created with its path, a list of entities, and any migration safety concerns.
Filename Convention
<number>_data_model_<snake_case_domain>.md
Examples:
6_data_model_user_billing.md
10_data_model_content_management.md
15_data_model_notification_system.md
Mandatory Columns
Every table must include these columns unless there is a documented reason to omit them:
| Column |
Type |
Purpose |
id |
Project convention (UUID/ULID/prefixed) |
Primary key |
created_at |
TIMESTAMP WITH TIME ZONE |
Row creation time, set by DB default NOW() |
updated_at |
TIMESTAMP WITH TIME ZONE |
Last modification time, updated by trigger or application |
deleted_at |
TIMESTAMP WITH TIME ZONE, nullable |
Soft delete marker; NULL means active |
Why Soft Deletes
Hard deletes cause:
- Data loss that cannot be recovered without backups
- Broken foreign key references or dangling orphans
- Inability to audit what was deleted and when
- Customer support cannot investigate deleted records
Use deleted_at IS NULL in application queries (or a default scope/view). Periodically archive or purge soft-deleted records older than a retention threshold.
When to Skip Soft Deletes
- High-volume transient data (logs, events, metrics) where retention is managed by TTL
- Join tables where the relationship itself has no independent identity
- Document this exemption in the data model
Data Model Document Format
# Data Model: <Domain Name>
Last updated: <YYYY-MM-DD>
## Overview
<1-2 sentences describing the domain and its purpose.>
## Entity Relationship Diagram
```mermaid
erDiagram
USERS ||--o{ ORDERS : places
USERS {
uuid id PK
string email UK
string name
timestamp created_at
timestamp updated_at
timestamp deleted_at
}
ORDERS ||--|{ ORDER_ITEMS : contains
ORDERS {
uuid id PK
uuid user_id FK
string status
decimal total_amount
timestamp created_at
timestamp updated_at
timestamp deleted_at
}
ORDER_ITEMS {
uuid id PK
uuid order_id FK
uuid product_id FK
integer quantity
decimal unit_price
timestamp created_at
timestamp updated_at
timestamp deleted_at
}
Entity Definitions
users
| Column |
Type |
Constraints |
Default |
Description |
id |
UUID |
PK |
gen_random_uuid() |
Unique identifier |
email |
VARCHAR(255) |
NOT NULL, UNIQUE |
— |
Login email, case-insensitive |
name |
VARCHAR(255) |
NOT NULL |
— |
Display name |
created_at |
TIMESTAMPTZ |
NOT NULL |
NOW() |
Row creation time |
updated_at |
TIMESTAMPTZ |
NOT NULL |
NOW() |
Last modification |
deleted_at |
TIMESTAMPTZ |
— |
NULL |
Soft delete marker |
orders
...
Relationships
| Relationship |
Type |
FK Column |
ON DELETE |
Notes |
| users -> orders |
One-to-many |
orders.user_id |
RESTRICT |
User cannot be deleted with active orders |
| orders -> order_items |
One-to-many |
order_items.order_id |
CASCADE |
Items deleted with order |
Indexes
| Table |
Index |
Columns |
Type |
Rationale |
users |
idx_users_email |
email |
Unique |
Login lookup |
orders |
idx_orders_user_id |
user_id |
B-tree |
List orders by user |
orders |
idx_orders_status_created |
status, created_at |
Composite |
Filter by status + sort by date |
orders |
idx_orders_active |
id WHERE deleted_at IS NULL |
Partial |
Exclude soft-deleted from queries |
Query Patterns
| Query |
Used By |
Index |
Notes |
| Get user by email |
Auth service |
idx_users_email |
— |
| List user's orders by date |
User dashboard |
idx_orders_user_id + idx_orders_status_created |
— |
Migration Safety Analysis
| Operation |
Table Size Risk |
Locking |
Safe Alternative |
Downtime |
Create orders table |
N/A (new) |
No lock |
— |
None |
Add status column to users |
Large table |
Brief lock |
Add with DEFAULT, no NOT NULL initially |
None |
Add index on orders.user_id |
Medium table |
Locks writes |
CREATE INDEX CONCURRENTLY |
None |
Audit Trail
<If the domain requires audit logging, describe the strategy.>
| What is Audited |
How |
Retention |
| Order status changes |
order_status_history table |
2 years |
| User profile updates |
audit_log table with before/after JSON |
1 year |
## Index Design Rules
1. **Always index foreign keys** -- Every FK column gets a B-tree index. Without it, JOINs and cascade deletes cause full table scans.
2. **Composite indexes: most selective column first** -- Put the column with highest cardinality first in the index definition.
3. **Partial indexes for soft deletes** -- `CREATE INDEX idx_active ON table(id) WHERE deleted_at IS NULL` avoids indexing deleted rows.
4. **Do not over-index** -- Each index slows writes. Only create indexes for proven query patterns, not speculative ones.
5. **Unique indexes for business constraints** -- If `(user_id, email)` must be unique, enforce it at the database level, not just the application.
## ON DELETE Strategy
| Relationship Type | Recommended ON DELETE | Rationale |
|------------------|----------------------|-----------|
| Parent-child (lifecycle dependency) | `CASCADE` | Child has no meaning without parent |
| Reference (independent entities) | `RESTRICT` | Prevent orphaning; force explicit cleanup |
| Optional reference | `SET NULL` | FK becomes NULL; referenced entity is independent |
| Audit/history records | `RESTRICT` | Never delete audit trail |
## Migration Safety Rules
Large table operations (>1M rows) require special handling:
| Operation | Risk | Safe Alternative |
|-----------|------|-----------------|
| `ALTER TABLE ADD COLUMN NOT NULL` | Rewrites table, locks | Add nullable, backfill, then add NOT NULL constraint |
| `ALTER TABLE ADD COLUMN DEFAULT` | PostgreSQL 11+ is safe; older versions rewrite | Check DB version first |
| `CREATE INDEX` | Locks writes | `CREATE INDEX CONCURRENTLY` |
| `ALTER TABLE DROP COLUMN` | Quick but irreversible | Rename to `_deprecated_<name>`, drop later |
| `ALTER TABLE ALTER TYPE` | Full table rewrite | Add new column, backfill, swap |
| `ALTER TABLE ADD CONSTRAINT FK` | Scans entire table | Add with `NOT VALID`, validate separately |
## Anti-patterns
- **No indexes on foreign keys** -- Every foreign key column must have an index. Without it, JOINs degrade to O(n) scans and cascade deletes lock the table. This is the most common and most damaging data model mistake.
- **No timestamps** -- Every table needs `created_at` and `updated_at`. Without them, debugging, auditing, and data analysis are impossible. There is no "we don't need timestamps" -- you always do.
- **Hard deletes** -- Using `DELETE FROM` destroys data permanently. Use soft deletes (`deleted_at`) for all user-facing data. Only use hard deletes for transient, high-volume data where retention is managed separately.
- **Nullable foreign keys without documented reason** -- A nullable FK means the relationship is optional. This must be an intentional design choice, not a default. Document why the relationship can be absent.
- **No migration safety review** -- Adding a NOT NULL column to a table with 10M rows will lock the table for minutes. Every migration must be reviewed for locking behavior, especially on large tables.
- **Storing derived data without a refresh strategy** -- Denormalized columns (e.g., `order_count` on users) are fine for performance, but you must document how and when they are refreshed. Stale denormalized data is worse than no denormalization.
- **Using ENUMs in the database** -- Database-level ENUMs are hard to modify (ALTER TYPE requires careful handling). Use VARCHAR with application-level validation, or a reference/lookup table.
- **Missing unique constraints for business rules** -- If an email must be unique per organization, enforce it with a unique index `(org_id, email)`, not just application code. Application bugs bypass code checks; database constraints do not.
1---2name: create-data-model3description: Design a data model from feature requirements when the user asks to create a schema, design database tables, model entities, plan a data layer, or create an ERD4---5
6# Create Data Model
7
8## Overview
9
10Design a production-ready data model from feature requirements, including entity definitions, relationships, a Mermaid ERD, index recommendations, and migration safety analysis. The output encodes senior-level database design practices: timestamps on every table, soft deletes, audit trails, and safe migration strategies.
11
12## Workflow
13
141. **Read project context** -- Read `.chalk/docs/engineering/` for:
15 - Existing data model documents to understand current schema and naming conventions
16 - Architecture docs for database technology (PostgreSQL, MySQL, MongoDB, etc.)
17 - ADRs or RFCs related to data storage decisions
18 - If no docs exist, scan the codebase for migration files, model definitions, or schema files to infer conventions
19
202. **Scan existing schema** -- Use Grep to find:
21 - Migration files or schema definitions to understand current tables and naming patterns
22 - ORM model definitions (ActiveRecord, SQLAlchemy, Prisma, TypeORM, Drizzle, etc.)
23 - Column naming conventions (snake_case, camelCase)
24 - Existing timestamp patterns (`created_at`/`updated_at` vs. `createdAt`/`updatedAt`)
25 - Soft delete patterns (e.g., `deleted_at`, `is_deleted`)
26 - ID strategy (auto-increment, UUID, ULID, prefixed IDs)
27
283. **Determine the next document number** -- List files in `.chalk/docs/engineering/` matching `*_data_model_*.md`. Find the highest number and increment by 1.
29
304. **Clarify the domain** -- From `$ARGUMENTS` and conversation context, identify:
31 - The entities to model and their real-world meaning
32 - The relationships between entities (one-to-one, one-to-many, many-to-many)
33 - The key queries this model must support efficiently
34 - Expected data volume and growth rate (this drives index and partitioning decisions)
35 - Ask the user for clarification if entity boundaries are unclear
36
375. **Design the entities** -- For each entity, define:
38 - Table name (plural, snake_case -- or match project convention)
39 - All columns with types, constraints, and defaults
40 - Primary key strategy (match existing convention)
41 - Required standard columns (see Mandatory Columns below)
42 - Foreign keys with appropriate ON DELETE behavior
43
446. **Design relationships** -- Map all relationships:
45 - One-to-many: foreign key on the "many" side
46 - Many-to-many: explicit join table with its own timestamps and potential payload columns
47 - One-to-one: foreign key with unique constraint -- decide which side owns the FK
48 - Polymorphic associations: prefer separate FKs over `type`/`id` pattern when possible
49
507. **Create the ERD** -- Generate a Mermaid ERD diagram showing all entities, their key columns, and relationships.
51
528. **Recommend indexes** -- Based on the expected query patterns, recommend:
53 - Primary key indexes (automatic)
54 - Foreign key indexes (always)
55 - Composite indexes for common query combinations
56 - Partial indexes for filtered queries (e.g., `WHERE deleted_at IS NULL`)
57 - Unique indexes for business-level uniqueness constraints
58
599. **Analyze migration safety** -- For each schema change, evaluate:
60 - Will this lock a table during migration? (ALTER TABLE on large tables)
61 - Is the migration reversible?
62 - Does it require a data backfill?
63 - Can it be deployed without downtime?
64
6510. **Write the document** -- Save to `.chalk/docs/engineering/<n>_data_model_<domain_slug>.md`.
66
6711. **Confirm** -- Tell the user the data model was created with its path, a list of entities, and any migration safety concerns.
68
69## Filename Convention
70
71```
72<number>_data_model_<snake_case_domain>.md
73```
74
75Examples:
76- `6_data_model_user_billing.md`
77- `10_data_model_content_management.md`
78- `15_data_model_notification_system.md`
79
80## Mandatory Columns
81
82Every table must include these columns unless there is a documented reason to omit them:
83
84| Column | Type | Purpose |
85|--------|------|---------|
86| `id` | Project convention (UUID/ULID/prefixed) | Primary key |
87| `created_at` | `TIMESTAMP WITH TIME ZONE` | Row creation time, set by DB default `NOW()` |
88| `updated_at` | `TIMESTAMP WITH TIME ZONE` | Last modification time, updated by trigger or application |
89| `deleted_at` | `TIMESTAMP WITH TIME ZONE`, nullable | Soft delete marker; `NULL` means active |
90
91### Why Soft Deletes
92
93Hard deletes cause:
94- Data loss that cannot be recovered without backups
95- Broken foreign key references or dangling orphans
96- Inability to audit what was deleted and when
97- Customer support cannot investigate deleted records
98
99Use `deleted_at IS NULL` in application queries (or a default scope/view). Periodically archive or purge soft-deleted records older than a retention threshold.
100
101### When to Skip Soft Deletes
102
103- High-volume transient data (logs, events, metrics) where retention is managed by TTL
104- Join tables where the relationship itself has no independent identity
105- Document this exemption in the data model
106
107## Data Model Document Format
108
109```markdown
110# Data Model: <Domain Name>
111
112Last updated: <YYYY-MM-DD>
113
114## Overview
115
116<1-2 sentences describing the domain and its purpose.>
117
118## Entity Relationship Diagram
119
120```mermaid
121erDiagram
122 USERS ||--o{ ORDERS : places
123 USERS {
124 uuid id PK
125 string email UK
126 string name
127 timestamp created_at
128 timestamp updated_at
129 timestamp deleted_at
130 }
131 ORDERS ||--|{ ORDER_ITEMS : contains
132 ORDERS {
133 uuid id PK
134 uuid user_id FK
135 string status
136 decimal total_amount
137 timestamp created_at
138 timestamp updated_at
139 timestamp deleted_at
140 }
141 ORDER_ITEMS {
142 uuid id PK
143 uuid order_id FK
144 uuid product_id FK
145 integer quantity
146 decimal unit_price
147 timestamp created_at
148 timestamp updated_at
149 timestamp deleted_at
150 }
151```
152
153## Entity Definitions
154
155### users
156
157<Purpose of this entity.>
158
159| Column | Type | Constraints | Default | Description |
160|--------|------|------------|---------|-------------|
161| `id` | `UUID` | `PK` | `gen_random_uuid()` | Unique identifier |
162| `email` | `VARCHAR(255)` | `NOT NULL, UNIQUE` | — | Login email, case-insensitive |
163| `name` | `VARCHAR(255)` | `NOT NULL` | — | Display name |
164| `created_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | Row creation time |
165| `updated_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | Last modification |
166| `deleted_at` | `TIMESTAMPTZ` | — | `NULL` | Soft delete marker |
167
168### orders
169
170...
171
172## Relationships
173
174| Relationship | Type | FK Column | ON DELETE | Notes |
175|-------------|------|-----------|-----------|-------|
176| users -> orders | One-to-many | `orders.user_id` | `RESTRICT` | User cannot be deleted with active orders |
177| orders -> order_items | One-to-many | `order_items.order_id` | `CASCADE` | Items deleted with order |
178
179## Indexes
180
181| Table | Index | Columns | Type | Rationale |
182|-------|-------|---------|------|-----------|
183| `users` | `idx_users_email` | `email` | Unique | Login lookup |
184| `orders` | `idx_orders_user_id` | `user_id` | B-tree | List orders by user |
185| `orders` | `idx_orders_status_created` | `status, created_at` | Composite | Filter by status + sort by date |
186| `orders` | `idx_orders_active` | `id` WHERE `deleted_at IS NULL` | Partial | Exclude soft-deleted from queries |
187
188## Query Patterns
189
190<List the primary queries this model supports and which indexes serve them.>
191
192| Query | Used By | Index | Notes |
193|-------|---------|-------|-------|
194| Get user by email | Auth service | `idx_users_email` | — |
195| List user's orders by date | User dashboard | `idx_orders_user_id` + `idx_orders_status_created` | — |
196
197## Migration Safety Analysis
198
199| Operation | Table Size Risk | Locking | Safe Alternative | Downtime |
200|-----------|---------------|---------|-----------------|----------|
201| Create `orders` table | N/A (new) | No lock | — | None |
202| Add `status` column to `users` | Large table | Brief lock | Add with DEFAULT, no NOT NULL initially | None |
203| Add index on `orders.user_id` | Medium table | Locks writes | `CREATE INDEX CONCURRENTLY` | None |
204
205## Audit Trail
206
207<If the domain requires audit logging, describe the strategy.>
208
209| What is Audited | How | Retention |
210|----------------|-----|-----------|
211| Order status changes | `order_status_history` table | 2 years |
212| User profile updates | `audit_log` table with before/after JSON | 1 year |
213```
214
215## Index Design Rules
216
2171. **Always index foreign keys** -- Every FK column gets a B-tree index. Without it, JOINs and cascade deletes cause full table scans.
2182. **Composite indexes: most selective column first** -- Put the column with highest cardinality first in the index definition.
2193. **Partial indexes for soft deletes** -- `CREATE INDEX idx_active ON table(id) WHERE deleted_at IS NULL` avoids indexing deleted rows.
2204. **Do not over-index** -- Each index slows writes. Only create indexes for proven query patterns, not speculative ones.
2215. **Unique indexes for business constraints** -- If `(user_id, email)` must be unique, enforce it at the database level, not just the application.
222
223## ON DELETE Strategy
224
225| Relationship Type | Recommended ON DELETE | Rationale |
226|------------------|----------------------|-----------|
227| Parent-child (lifecycle dependency) | `CASCADE` | Child has no meaning without parent |
228| Reference (independent entities) | `RESTRICT` | Prevent orphaning; force explicit cleanup |
229| Optional reference | `SET NULL` | FK becomes NULL; referenced entity is independent |
230| Audit/history records | `RESTRICT` | Never delete audit trail |
231
232## Migration Safety Rules
233
234Large table operations (>1M rows) require special handling:
235
236| Operation | Risk | Safe Alternative |
237|-----------|------|-----------------|
238| `ALTER TABLE ADD COLUMN NOT NULL` | Rewrites table, locks | Add nullable, backfill, then add NOT NULL constraint |
239| `ALTER TABLE ADD COLUMN DEFAULT` | PostgreSQL 11+ is safe; older versions rewrite | Check DB version first |
240| `CREATE INDEX` | Locks writes | `CREATE INDEX CONCURRENTLY` |
241| `ALTER TABLE DROP COLUMN` | Quick but irreversible | Rename to `_deprecated_<name>`, drop later |
242| `ALTER TABLE ALTER TYPE` | Full table rewrite | Add new column, backfill, swap |
243| `ALTER TABLE ADD CONSTRAINT FK` | Scans entire table | Add with `NOT VALID`, validate separately |
244
245## Anti-patterns
246
247- **No indexes on foreign keys** -- Every foreign key column must have an index. Without it, JOINs degrade to O(n) scans and cascade deletes lock the table. This is the most common and most damaging data model mistake.
248- **No timestamps** -- Every table needs `created_at` and `updated_at`. Without them, debugging, auditing, and data analysis are impossible. There is no "we don't need timestamps" -- you always do.
249- **Hard deletes** -- Using `DELETE FROM` destroys data permanently. Use soft deletes (`deleted_at`) for all user-facing data. Only use hard deletes for transient, high-volume data where retention is managed separately.
250- **Nullable foreign keys without documented reason** -- A nullable FK means the relationship is optional. This must be an intentional design choice, not a default. Document why the relationship can be absent.
251- **No migration safety review** -- Adding a NOT NULL column to a table with 10M rows will lock the table for minutes. Every migration must be reviewed for locking behavior, especially on large tables.
252- **Storing derived data without a refresh strategy** -- Denormalized columns (e.g., `order_count` on users) are fine for performance, but you must document how and when they are refreshed. Stale denormalized data is worse than no denormalization.
253- **Using ENUMs in the database** -- Database-level ENUMs are hard to modify (ALTER TYPE requires careful handling). Use VARCHAR with application-level validation, or a reference/lookup table.
254- **Missing unique constraints for business rules** -- If an email must be unique per organization, enforce it with a unique index `(org_id, email)`, not just application code. Application bugs bypass code checks; database constraints do not.