Files
Meal-Planner/docs/database-schema.md
T
admin 0c5b0aa5ed docs: add complete project documentation
- SPEC.md: project specification and goals
- ARCHITECTURE.md: system design and component descriptions
- database-schema.md: PostgreSQL schema with all tables
- implementation-plan.md: 12-phase implementation guide
- RUNNING.md: deployment and troubleshooting guide
- ORIENTATION.md: context compaction recovery guide
- README.md: project overview and quick start

Family profile: 2 adults, 2 children. Mushroom avoidance for 3/4.
Approval workflow: email proposals, one denial swaps meal.
Tech stack: FastAPI, PostgreSQL, React, Playwright, SendGrid.
2026-05-04 19:27:22 -07:00

11 KiB

Meal Planner - Database Schema

1. Schema Overview

PostgreSQL 15+ with the following extensions:

  • uuid-ossp for UUID generation
  • pg_trgm for fuzzy text search (if needed)

2. Tables

2.1 family_profile

Primary household configuration.

Column Type Constraints Description
id UUID PK, default uuid_generate_v4() Primary key
name VARCHAR(100) NOT NULL Household name
household_size INTEGER NOT NULL, CHECK (household_size > 0) Number of people
adult_count INTEGER NOT NULL Number of adults
child_count INTEGER NOT NULL Number of children
dietary_notes TEXT Free-text dietary notes
budget_per_meal NUMERIC(10,2) DEFAULT 50.00 Budget target per meal (in dollars)
calorie_target INTEGER Daily calorie target per adult
created_at TIMESTAMPTZ DEFAULT NOW()
updated_at TIMESTAMPTZ DEFAULT NOW()

2.2 ingredient

Master list of all ingredients.

Column Type Constraints Description
id UUID PK
name VARCHAR(200) NOT NULL, UNIQUE Ingredient name
plural_name VARCHAR(200) For shopping list grouping
aisle VARCHAR(100) Lucky California aisle
typical_price NUMERIC(10,2) Price per unit
unit VARCHAR(50) e.g., "lb", "oz", "bunch"
season_months INTEGER[] Array of month numbers 1-12
created_at TIMESTAMPTZ DEFAULT NOW()

2.3 recipe

All recipes in the system.

Column Type Constraints Description
id UUID PK
family_profile_id UUID FK → family_profile(id) Optional, for family-specific recipes
name VARCHAR(300) NOT NULL Recipe name
description TEXT Short description
image_url TEXT URL to recipe image
image_source VARCHAR(50) 'scraped', 'ai_generated', 'manual'
prep_time_minutes INTEGER Prep time
cook_time_minutes INTEGER Cook time
total_time_minutes INTEGER Computed: prep + cook
servings INTEGER NOT NULL
servings_scaled INTEGER For scaling recipes
cuisine_tags VARCHAR(50)[] Array: 'italian', 'asian', etc.
dietary_tags VARCHAR(50)[] Array: 'vegetarian', 'gluten_free', etc.
protein_type VARCHAR(50) 'chicken', 'beef', 'vegetarian', 'seafood'
spice_level INTEGER CHECK (spice_level BETWEEN 1 AND 5) 1=mild, 5=very spicy
ingredients JSONB NOT NULL [{ingredient_id, quantity, unit, is_optional}]
instructions TEXT[] NOT NULL Array of step strings
source_url TEXT Original recipe URL if scraped
scraped_at TIMESTAMPTZ When originally scraped
is_manually_added BOOLEAN DEFAULT FALSE User-created vs scraped
created_at TIMESTAMPTZ DEFAULT NOW()
updated_at TIMESTAMPTZ DEFAULT NOW()

2.4 meal_plan

A generated weekly meal plan.

Column Type Constraints Description
id UUID PK
family_profile_id UUID FK → family_profile(id)
week_start_date DATE NOT NULL Monday of the week
status VARCHAR(20) NOT NULL, DEFAULT 'draft' 'draft', 'pending_approval', 'approved', 'locked'
approval_deadline TIMESTAMPTZ When approval period ends
total_estimated_cost NUMERIC(10,2) Sum of all meal costs
notes TEXT Admin notes
created_at TIMESTAMPTZ DEFAULT NOW()
updated_at TIMESTAMPTZ DEFAULT NOW()

Unique constraint: (family_profile_id, week_start_date)

2.5 meal_plan_item

Individual meal within a plan.

Column Type Constraints Description
id UUID PK
meal_plan_id UUID FK → meal_plan(id) ON DELETE CASCADE
recipe_id UUID FK → recipe(id)
day_of_week INTEGER NOT NULL, CHECK (day_of_week BETWEEN 0 AND 6) 0=Monday, 6=Sunday
meal_type VARCHAR(20) NOT NULL 'breakfast', 'lunch', 'dinner'
approval_status VARCHAR(20) DEFAULT 'pending' 'pending', 'approved', 'denied', 'swapped'
approval_token UUID UNIQUE, DEFAULT uuid_generate_v4() Token for email approval links
approval_token_expires TIMESTAMPTZ
denial_reason VARCHAR(50) 'too_expensive', 'boring', 'disliked_ingredient', 'other'
denial_details TEXT Free-text explanation
estimated_cost NUMERIC(10,2) Per serving cost
used_pantry_items UUID[] Home pantry items used
created_at TIMESTAMPTZ DEFAULT NOW()
updated_at TIMESTAMPTZ DEFAULT NOW()

Unique constraint: (meal_plan_id, day_of_week, meal_type)

2.6 home_pantry

Items the family has at home.

Column Type Constraints Description
id UUID PK
family_profile_id UUID FK → family_profile(id) ON DELETE CASCADE
ingredient_id UUID FK → ingredient(id)
quantity NUMERIC(10,2) How much on hand
unit VARCHAR(50) e.g., "cans", "lb"
expires_at DATE Perishable expiry date
added_at TIMESTAMPTZ DEFAULT NOW()
created_at TIMESTAMPTZ DEFAULT NOW()

Unique constraint: (family_profile_id, ingredient_id)

2.7 feedback

Meal feedback and ratings.

Column Type Constraints Description
id UUID PK
family_profile_id UUID FK → family_profile(id)
meal_plan_item_id UUID FK → meal_plan_item(id) ON DELETE CASCADE
rating INTEGER CHECK (rating BETWEEN 1 AND 5) 1-5 stars
never_suggest BOOLEAN DEFAULT FALSE Add to "never suggest" list
denial_reason VARCHAR(50) Same as meal_plan_item
feedback_text TEXT Free-text feedback
created_at TIMESTAMPTZ DEFAULT NOW()

Unique constraint: (meal_plan_item_id) — one feedback per meal

2.8 never_suggest

Global "never suggest" flags per family.

Column Type Constraints Description
id UUID PK
family_profile_id UUID FK → family_profile(id) ON DELETE CASCADE
ingredient_id UUID FK → ingredient(id) Optionally block specific ingredients
recipe_id UUID FK → recipe(id) Or block entire recipes
reason VARCHAR(50) 'allergy', 'dislike', 'tried_too_much', 'other'
notes TEXT
created_at TIMESTAMPTZ DEFAULT NOW()

2.9 grocery_item

Scraped items from Lucky California.

Column Type Constraints Description
id UUID PK
name VARCHAR(300) NOT NULL Product name
brand VARCHAR(200) Brand name
current_price NUMERIC(10,2) Current sale price
regular_price NUMERIC(10,2) Normal price
unit VARCHAR(50) Price unit
aisle VARCHAR(100) Store aisle
image_url TEXT Product image
product_url TEXT Lucky California product page
is_on_sale BOOLEAN DEFAULT FALSE Currently on sale
sale_start_date DATE
sale_end_date DATE
in_season BOOLEAN DEFAULT FALSE Currently in season
scraped_at TIMESTAMPTZ DEFAULT NOW()
scraped_url TEXT Source URL

Index: CREATE INDEX idx_grocery_item_is_on_sale ON grocery_item(is_on_sale) WHERE is_on_sale = TRUE

2.10 scrape_log

Scraping operation history.

Column Type Constraints Description
id UUID PK
source VARCHAR(50) NOT NULL 'lucky_california', 'recipe_site'
scrape_type VARCHAR(50) NOT NULL 'weekly_ad', 'product_catalog', 'recipe'
status VARCHAR(20) NOT NULL 'started', 'success', 'failed'
items_scraped INTEGER DEFAULT 0
error_message TEXT
started_at TIMESTAMPTZ DEFAULT NOW()
completed_at TIMESTAMPTZ
duration_seconds INTEGER

2.11 email_log

Email sending history.

Column Type Constraints Description
id UUID PK
recipient_email VARCHAR(300) NOT NULL
recipient_name VARCHAR(200)
template VARCHAR(100) NOT NULL 'meal_proposal', 'reminder', 'confirmation'
meal_plan_id UUID FK → meal_plan(id) Related meal plan
meal_plan_item_id UUID FK → meal_plan_item(id) Optional: specific meal
sendgrid_message_id VARCHAR(100) SendGrid message ID
status VARCHAR(20) NOT NULL 'sent', 'delivered', 'failed', 'bounced'
error_message TEXT
created_at TIMESTAMPTZ DEFAULT NOW()
delivered_at TIMESTAMPTZ

3. Indexes

3.1 Primary Indexes

All PKs have default B-tree indexes.

3.2 Foreign Key Indexes

CREATE INDEX idx_recipe_family_profile ON recipe(family_profile_id);
CREATE INDEX idx_meal_plan_family_profile ON meal_plan(family_profile_id);
CREATE INDEX idx_meal_plan_item_meal_plan ON meal_plan_item(meal_plan_id);
CREATE INDEX idx_meal_plan_item_recipe ON meal_plan_item(recipe_id);
CREATE INDEX idx_home_pantry_family_profile ON home_pantry(family_profile_id);
CREATE INDEX idx_feedback_family_profile ON feedback(family_profile_id);
CREATE INDEX idx_feedback_meal_plan_item ON feedback(meal_plan_item_id);
CREATE INDEX idx_never_suggest_family_profile ON never_suggest(family_profile_id);
CREATE INDEX idx_grocery_item_sale ON grocery_item(is_on_sale) WHERE is_on_sale = TRUE;

3.3 Full-Text Search Indexes

CREATE INDEX idx_ingredient_name_fts ON ingredient USING gin(to_tsvector('english', name));
CREATE INDEX idx_recipe_name_fts ON recipe USING gin(to_tsvector('english', name));

4. Data Migration Strategy

4.1 Initial Schema

Use SQLAlchemy or Alembic for schema management.

4.2 Future Migrations

  • Alembic for version-controlled migrations
  • All migrations must be reversible

4.3 Seed Data

  • Basic ingredient list pre-populated
  • Sample recipes for initial testing (5-10 meals)
  • Default family profile template

5. Data Retention

Data Type Retention Action After Expiry
Meal plans 12 weeks Archive to JSON, delete rows
Feedback Indefinite Keep for learning
Scraped grocery items 2 weeks Delete old items
Scrape logs 30 days Delete old logs
Email logs 90 days Delete old logs
Never-suggest Indefinite Keep

6. Row-Level Security (Future)

If multi-family support is added:

  • RLS on all tables
  • Policies based on family_profile_id
  • Backend enforces tenant isolation