Page:
Database Schema
No results
1
Database Schema
sscoble edited this page 2026-05-02 13:41:10 -07:00
Table of Contents
Database Schema
WardrobeAI MVP uses SQLite with 6 tables.
Database file: backend/database/wardrobe-ai.db
Initialization: backend/src/db/init.js (creates all tables on server start)
Tables
style_profiles
Stores style preference profiles per user. Acts as the "brief" the matching algorithm uses to select items.
| Column | Type | Description |
|---|---|---|
id |
TEXT PK | Unique identifier (e.g., profile_001) |
name |
TEXT NOT NULL | Human-readable name (e.g., "Work Professional") |
description |
TEXT | Free-text description of the style profile |
budget |
REAL | Monthly budget in USD |
must_have_categories |
TEXT | JSON array of required categories (e.g., ["tops", "bottoms"]) |
preferred_styles |
TEXT | JSON array of style tags (e.g., ["professional", "classic"]) |
excluded_styles |
TEXT | JSON array of excluded style tags |
max_price |
REAL | Maximum price per item |
preferred_colors |
TEXT | JSON array of color preferences |
seasons |
TEXT | JSON array of applicable seasons |
created_at |
DATETIME | Default CURRENT_TIMESTAMP |
Sample data (5 profiles):
| ID | Name | Budget | Key Styles |
|---|---|---|---|
| profile_001 | Work Professional | $150 | professional, business formal, classic |
| profile_002 | Casual Weekend | $80 | casual, bohemian, preppy |
| profile_003 | Formal Events | $200 | elegant, evening, formal |
| profile_004 | Summer Garden Party | $100 | casual, bohemian, summer |
| profile_005 | Winter Professional | $180 | professional, classic, business casual |
inventory_items
Catalog of clothing items available for curation.
| Column | Type | Description |
|---|---|---|
id |
TEXT PK | Unique identifier (e.g., item_001) |
name |
TEXT NOT NULL | Product name |
price |
REAL NOT NULL | Price in USD |
category |
TEXT | Category: tops, bottoms, dresses, outerwear |
colors |
TEXT | JSON array of colors (e.g., ["white", "black"]) |
styles |
TEXT | JSON array of style tags (e.g., ["classic", "casual"]) |
brand |
TEXT | Brand name |
description |
TEXT | Product description |
image_path |
TEXT | Path to product image |
created_at |
DATETIME | Default CURRENT_TIMESTAMP |
Sample data (20 items):
| ID | Name | Price | Category |
|---|---|---|---|
| item_001 | Classic White Blouse | $49.99 | tops |
| item_002 | Denim A-Line Skirt | $59.99 | bottoms |
| item_003 | Silk Cocktail Dress | $129.99 | dresses |
| item_004 | Cashmere Sweater | $89.99 | tops |
| item_005 | Floral Summer Dress | $69.99 | dresses |
| ... | ... | ... | ... |
user_profiles
User account information and physical measurements.
| Column | Type | Description |
|---|---|---|
id |
TEXT PK | Unique identifier |
name |
TEXT NOT NULL | User's display name |
body_type |
TEXT | Hourglass, pear, apple, rectangle |
height |
TEXT | Height (e.g., 5'7") |
size |
TEXT | Clothing size |
style_preferences |
TEXT | JSON string of preferences |
budget_range |
TEXT | Budget range description |
created_at |
DATETIME | Default CURRENT_TIMESTAMP |
updated_at |
DATETIME | Default CURRENT_TIMESTAMP |
curated_boxes
Generated box shipments linking users to their curated items.
| Column | Type | Description |
|---|---|---|
id |
TEXT PK | Auto-generated (e.g., box_1714672800000_abc123xyz) |
user_profile_id |
TEXT NOT NULL | FK → user_profiles(id) |
style_profile_id |
TEXT NOT NULL | FK → style_profiles(id) |
items |
TEXT | JSON array of item objects with scores |
total_price |
REAL | Sum of all item prices in the box |
created_at |
DATETIME | Default CURRENT_TIMESTAMP |
feedback
User feedback on individual items within a curated box.
| Column | Type | Description |
|---|---|---|
id |
TEXT PK | Auto-generated |
box_id |
TEXT NOT NULL | FK → curated_boxes(id) |
item_id |
TEXT NOT NULL | Item being reviewed |
rating |
INTEGER | 1-5 rating (CHECK constraint) |
kept |
BOOLEAN | Whether the item was kept |
returned |
BOOLEAN | Whether the item was returned |
notes |
TEXT | Free-text comments |
created_at |
DATETIME | Default CURRENT_TIMESTAMP |
matching_history
Tracks each matching decision for analytics and algorithm improvement.
| Column | Type | Description |
|---|---|---|
id |
TEXT PK | Auto-generated |
user_profile_id |
TEXT NOT NULL | FK → user_profiles(id) |
item_id |
TEXT NOT NULL | Item that was matched |
match_score |
INTEGER | Score 0-100 |
match_reasons |
TEXT | JSON array of reason strings |
created_at |
DATETIME | Default CURRENT_TIMESTAMP |
Entity Relationship Diagram
user_profiles ──1:N── curated_boxes ──1:N── feedback
│ │
│ └── item_id → inventory_items
│
└── style_profile_id → style_profiles
matching_history ── user_profile_id → user_profiles
└── item_id → inventory_items
Example Queries
Get all items in a user's most recent box
SELECT cb.id AS box_id, ii.name, ii.price, cb.total_price
FROM curated_boxes cb
JOIN inventory_items ii ON ii.id IN (
SELECT value FROM json_each(cb.items, '$[*].id')
)
WHERE cb.user_profile_id = 'user_001'
ORDER BY cb.created_at DESC
LIMIT 1;
Get average ratings per category
SELECT ii.category, AVG(f.rating) as avg_rating, COUNT(*) as feedback_count
FROM feedback f
JOIN inventory_items ii ON f.item_id = ii.id
GROUP BY ii.category
ORDER BY avg_rating DESC;
Find items with highest keep rate
SELECT
ii.name,
SUM(CASE WHEN f.kept = 1 THEN 1 ELSE 0 END) * 1.0 / COUNT(*) as keep_rate,
AVG(f.rating) as avg_rating
FROM feedback f
JOIN inventory_items ii ON f.item_id = ii.id
GROUP BY f.item_id
HAVING COUNT(*) >= 2
ORDER BY keep_rate DESC;
Show recent boxes by profile
SELECT
sp.name as profile_name,
cb.id as box_id,
cb.total_price,
cb.created_at
FROM curated_boxes cb
JOIN style_profiles sp ON cb.style_profile_id = sp.id
ORDER BY cb.created_at DESC;
Indexing Notes
- Primary keys are used on all
idcolumns - Foreign keys:
curated_boxes(user_profile_id),curated_boxes(style_profile_id),feedback(box_id),matching_history(user_profile_id) - For production scaling, add indexes on
curated_boxes.created_at,feedback.item_id, andmatching_history.created_at - JSON fields (
items,preferred_styles,colors, etc.) are stored as TEXT and parsed at the application layer viaJSON.parse()