Public Access
fix: address adversarial review blockers
All §1 consensus blockers and §2 high-risk gaps resolved: Schema fixes: - Remove RecipeIngredient join table, use JSONB for ingredients - Add family_member table for per-voter approval tracking - Add all ENUMs for status fields (no loose VARCHAR) - Add CHECK constraints (household_size, rating 1-5, day_of_week) - Add name_lower for case-insensitive ingredient matching - Add grocery_item → ingredient FK - Fix day_of_week to ISO-8601 (1=Monday, 7=Sunday) - Remove calorie_target (nutrition is non-goal) Approval flow redesign: - Email link → confirmation page (GET), not auto-approve - Actual vote is POST from confirmation page - Per-voter tokens (single-use, 72h TTL) - Record which member voted Auth model: - VPN-only for admin endpoints - Session-based for family web UI Docker hardening: - Remove direct port exposure for backend/frontend - nginx is sole entrypoint - Add docker-compose.dev.yml for local dev Skeleton fixes: - Add missing Pantry.tsx page - Add missing index.html (Vite entrypoint) - Add package-lock.json - Fix SQLAlchemy 2 text() for raw SQL - Remove create_all from startup (use migrations) - Configure Alembic properly Docs updates: - Update Lucky URL to luckysupermarkets.com - Add WCAG 2.1 AA accessibility target - Update family profile with correct mushroom preferences - Add external dependencies list to SPEC Verification: - docker compose config: PASS - docker compose build backend: PASS - docker compose build frontend: PASS - backend import: PASS - alembic context: PASS
This commit is contained in:
+158
-61
@@ -4,13 +4,29 @@
|
||||
|
||||
PostgreSQL 15+ with the following extensions:
|
||||
- `uuid-ossp` for UUID generation
|
||||
- `pg_trgm` for fuzzy text search (if needed)
|
||||
- `CITEXT` for case-insensitive text (ingredient names)
|
||||
|
||||
---
|
||||
|
||||
## 2. Tables
|
||||
## 2. Enums
|
||||
|
||||
### 2.1 `family_profile`
|
||||
```sql
|
||||
CREATE TYPE family_member_role_enum AS ENUM ('adult', 'child');
|
||||
CREATE TYPE meal_type_enum AS ENUM ('breakfast', 'lunch', 'dinner');
|
||||
CREATE TYPE meal_plan_status_enum AS ENUM ('draft', 'pending_approval', 'approved', 'locked');
|
||||
CREATE TYPE meal_plan_item_status_enum AS ENUM ('pending', 'approved', 'denied', 'swapped');
|
||||
CREATE TYPE approval_token_status_enum AS ENUM ('active', 'used', 'expired');
|
||||
CREATE TYPE denial_reason_enum AS ENUM ('too_expensive', 'boring', 'disliked_ingredient', 'cultural', 'other');
|
||||
CREATE TYPE never_suggest_reason_enum AS ENUM ('allergy', 'dislike', 'tried_too_much', 'other');
|
||||
CREATE TYPE scrape_status_enum AS ENUM ('started', 'success', 'failed');
|
||||
CREATE TYPE email_status_enum AS ENUM ('sent', 'delivered', 'failed', 'bounced');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Tables
|
||||
|
||||
### 3.1 `family_profile`
|
||||
|
||||
Primary household configuration.
|
||||
|
||||
@@ -23,18 +39,39 @@ Primary household configuration.
|
||||
| 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 |
|
||||
| calorie_target | INTEGER | | Daily calorie target per adult (aspirational) |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
| updated_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
|
||||
### 2.2 `ingredient`
|
||||
**Constraints**:
|
||||
- `CHECK (adult_count + child_count = household_size)`
|
||||
|
||||
Master list of all ingredients.
|
||||
### 3.2 `family_member`
|
||||
|
||||
Individual family members for per-person voting and preferences.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | |
|
||||
| name | VARCHAR(200) | NOT NULL, UNIQUE | Ingredient name |
|
||||
| family_profile_id | UUID | FK → family_profile(id) ON DELETE CASCADE | |
|
||||
| name | VARCHAR(100) | NOT NULL | Member name |
|
||||
| email | VARCHAR(300) | UNIQUE per family | Email for notifications |
|
||||
| role | family_member_role_enum | NOT NULL | 'adult' or 'child' |
|
||||
| likes_mushrooms | BOOLEAN | DEFAULT FALSE | Dietary preference |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
| updated_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
|
||||
**Unique constraint**: `(family_profile_id, email)`
|
||||
|
||||
### 3.3 `ingredient`
|
||||
|
||||
Master list of all ingredients. `name_lower` is used for case-insensitive matching.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | |
|
||||
| name | VARCHAR(200) | NOT NULL | Display name (e.g., "Carrots") |
|
||||
| name_lower | VARCHAR(200) | NOT NULL, UNIQUE | Lowercase for matching (e.g., "carrots") |
|
||||
| plural_name | VARCHAR(200) | | For shopping list grouping |
|
||||
| aisle | VARCHAR(100) | | Lucky California aisle |
|
||||
| typical_price | NUMERIC(10,2) | | Price per unit |
|
||||
@@ -42,9 +79,9 @@ Master list of all ingredients.
|
||||
| season_months | INTEGER[] | | Array of month numbers 1-12 |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
|
||||
### 2.3 `recipe`
|
||||
### 3.4 `recipe`
|
||||
|
||||
All recipes in the system.
|
||||
All recipes in the system. Ingredients stored as JSONB for flexibility.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
@@ -56,14 +93,13 @@ All recipes in the system.
|
||||
| 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 |
|
||||
| servings | INTEGER | NOT NULL | Default servings |
|
||||
| servings_scaled | INTEGER | | Current scaled servings |
|
||||
| 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}] |
|
||||
| ingredients | JSONB | NOT NULL | [{ingredient_id, name, 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 |
|
||||
@@ -71,7 +107,9 @@ All recipes in the system.
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
| updated_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
|
||||
### 2.4 `meal_plan`
|
||||
**Note**: `total_time_minutes` is computed as `prep_time_minutes + cook_time_minutes` (not stored).
|
||||
|
||||
### 3.5 `meal_plan`
|
||||
|
||||
A generated weekly meal plan.
|
||||
|
||||
@@ -79,8 +117,8 @@ A generated weekly meal plan.
|
||||
|--------|------|-------------|-------------|
|
||||
| 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' |
|
||||
| week_start_date | DATE | NOT NULL | Monday of the week (ISO: 1=Mon, 7=Sun) |
|
||||
| status | meal_plan_status_enum | NOT NULL, DEFAULT 'draft' | |
|
||||
| approval_deadline | TIMESTAMPTZ | | When approval period ends |
|
||||
| total_estimated_cost | NUMERIC(10,2) | | Sum of all meal costs |
|
||||
| notes | TEXT | | Admin notes |
|
||||
@@ -89,21 +127,19 @@ A generated weekly meal plan.
|
||||
|
||||
**Unique constraint**: `(family_profile_id, week_start_date)`
|
||||
|
||||
### 2.5 `meal_plan_item`
|
||||
### 3.6 `meal_plan_item`
|
||||
|
||||
Individual meal within a plan.
|
||||
Individual meal within a plan. `day_of_week` uses ISO-8601 convention (1=Monday through 7=Sunday).
|
||||
|
||||
| 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' |
|
||||
| day_of_week | INTEGER | NOT NULL, CHECK (day_of_week BETWEEN 1 AND 7) | 1=Mon, 7=Sun (ISO-8601) |
|
||||
| meal_type | meal_type_enum | NOT NULL | 'breakfast', 'lunch', 'dinner' |
|
||||
| approval_status | meal_plan_item_status_enum | DEFAULT 'pending' | |
|
||||
| denial_reason | denial_reason_enum | | If denied |
|
||||
| denial_details | TEXT | | Free-text explanation |
|
||||
| estimated_cost | NUMERIC(10,2) | | Per serving cost |
|
||||
| used_pantry_items | UUID[] | | Home pantry items used |
|
||||
@@ -112,7 +148,38 @@ Individual meal within a plan.
|
||||
|
||||
**Unique constraint**: `(meal_plan_id, day_of_week, meal_type)`
|
||||
|
||||
### 2.6 `home_pantry`
|
||||
### 3.7 `meal_plan_vote`
|
||||
|
||||
Per-member votes on meal plan items. This is the key to the redesigned approval flow.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | |
|
||||
| meal_plan_item_id | UUID | FK → meal_plan_item(id) ON DELETE CASCADE | |
|
||||
| family_member_id | UUID | FK → family_member(id) ON DELETE CASCADE | |
|
||||
| vote | BOOLEAN | NOT NULL | TRUE=approve, FALSE=deny |
|
||||
| voted_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
|
||||
**Unique constraint**: `(meal_plan_item_id, family_member_id)` — one vote per member per meal
|
||||
|
||||
### 3.8 `approval_token`
|
||||
|
||||
Single-use tokens for email approval links.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | |
|
||||
| meal_plan_item_id | UUID | FK → meal_plan_item(id) ON DELETE CASCADE | |
|
||||
| family_member_id | UUID | FK → family_member(id) ON DELETE CASCADE | |
|
||||
| token | VARCHAR(64) | NOT NULL, UNIQUE | Secure random token |
|
||||
| status | approval_token_status_enum | DEFAULT 'active' | |
|
||||
| expires_at | TIMESTAMPTZ | NOT NULL | Token expiration (72h from send) |
|
||||
| used_at | TIMESTAMPTZ | | When token was used |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
|
||||
**Unique constraint**: `(meal_plan_item_id, family_member_id)`
|
||||
|
||||
### 3.9 `home_pantry`
|
||||
|
||||
Items the family has at home.
|
||||
|
||||
@@ -120,7 +187,7 @@ Items the family has at home.
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | |
|
||||
| family_profile_id | UUID | FK → family_profile(id) ON DELETE CASCADE | |
|
||||
| ingredient_id | UUID | FK → ingredient(id) | |
|
||||
| ingredient_id | UUID | FK → ingredient(id) ON DELETE SET NULL | |
|
||||
| quantity | NUMERIC(10,2) | | How much on hand |
|
||||
| unit | VARCHAR(50) | | e.g., "cans", "lb" |
|
||||
| expires_at | DATE | | Perishable expiry date |
|
||||
@@ -129,24 +196,25 @@ Items the family has at home.
|
||||
|
||||
**Unique constraint**: `(family_profile_id, ingredient_id)`
|
||||
|
||||
### 2.7 `feedback`
|
||||
### 3.10 `feedback`
|
||||
|
||||
Meal feedback and ratings.
|
||||
Meal feedback and ratings. One feedback row per meal, can be associated with a specific member.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | |
|
||||
| family_profile_id | UUID | FK → family_profile(id) | |
|
||||
| family_member_id | UUID | FK → family_member(id) ON DELETE SET NULL | |
|
||||
| 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 |
|
||||
| denial_reason | denial_reason_enum | | 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
|
||||
**Note**: Multiple feedback rows can exist for the same meal (one per family member).
|
||||
|
||||
### 2.8 `never_suggest`
|
||||
### 3.11 `never_suggest`
|
||||
|
||||
Global "never suggest" flags per family.
|
||||
|
||||
@@ -154,19 +222,20 @@ Global "never suggest" flags per family.
|
||||
|--------|------|-------------|-------------|
|
||||
| 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' |
|
||||
| ingredient_id | UUID | FK → ingredient(id) ON DELETE CASCADE | Optionally block specific ingredients |
|
||||
| recipe_id | UUID | FK → recipe(id) ON DELETE CASCADE | Or block entire recipes |
|
||||
| reason | never_suggest_reason_enum | | |
|
||||
| notes | TEXT | | |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
|
||||
### 2.9 `grocery_item`
|
||||
### 3.12 `grocery_item`
|
||||
|
||||
Scraped items from Lucky California.
|
||||
Scraped items from Lucky California. Links to `ingredient` table.
|
||||
|
||||
| Column | Type | Constraints | Description |
|
||||
|--------|------|-------------|-------------|
|
||||
| id | UUID | PK | |
|
||||
| ingredient_id | UUID | FK → ingredient(id) ON DELETE SET NULL | Links to master ingredient list |
|
||||
| name | VARCHAR(300) | NOT NULL | Product name |
|
||||
| brand | VARCHAR(200) | | Brand name |
|
||||
| current_price | NUMERIC(10,2) | | Current sale price |
|
||||
@@ -182,9 +251,7 @@ Scraped items from Lucky California.
|
||||
| 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`
|
||||
### 3.13 `scrape_log`
|
||||
|
||||
Scraping operation history.
|
||||
|
||||
@@ -193,14 +260,14 @@ Scraping operation history.
|
||||
| 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' |
|
||||
| status | scrape_status_enum | NOT NULL | |
|
||||
| items_scraped | INTEGER | DEFAULT 0 | |
|
||||
| error_message | TEXT | | |
|
||||
| started_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
| completed_at | TIMESTAMPTZ | | |
|
||||
| duration_seconds | INTEGER | | |
|
||||
|
||||
### 2.11 `email_log`
|
||||
### 3.14 `email_log`
|
||||
|
||||
Email sending history.
|
||||
|
||||
@@ -213,71 +280,101 @@ Email sending history.
|
||||
| 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' |
|
||||
| status | email_status_enum | NOT NULL | |
|
||||
| error_message | TEXT | | |
|
||||
| created_at | TIMESTAMPTZ | DEFAULT NOW() | |
|
||||
| delivered_at | TIMESTAMPTZ | | |
|
||||
|
||||
---
|
||||
|
||||
## 3. Indexes
|
||||
## 4. Indexes
|
||||
|
||||
### 3.1 Primary Indexes
|
||||
### 4.1 Primary Indexes
|
||||
All PKs have default B-tree indexes.
|
||||
|
||||
### 3.2 Foreign Key Indexes
|
||||
### 4.2 Foreign Key Indexes
|
||||
```sql
|
||||
CREATE INDEX idx_family_member_profile ON family_member(family_profile_id);
|
||||
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_meal_plan_vote_item ON meal_plan_vote(meal_plan_item_id);
|
||||
CREATE INDEX idx_meal_plan_vote_member ON meal_plan_vote(family_member_id);
|
||||
CREATE INDEX idx_approval_token_item ON approval_token(meal_plan_item_id);
|
||||
CREATE INDEX idx_approval_token_member ON approval_token(family_member_id);
|
||||
CREATE INDEX idx_home_pantry_family_profile ON home_pantry(family_profile_id);
|
||||
CREATE INDEX idx_home_pantry_ingredient ON home_pantry(ingredient_id);
|
||||
CREATE INDEX idx_feedback_family_profile ON feedback(family_profile_id);
|
||||
CREATE INDEX idx_feedback_member ON feedback(family_member_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;
|
||||
CREATE INDEX idx_grocery_item_ingredient ON grocery_item(ingredient_id);
|
||||
CREATE INDEX idx_grocery_item_is_on_sale ON grocery_item(is_on_sale) WHERE is_on_sale = TRUE;
|
||||
```
|
||||
|
||||
### 3.3 Full-Text Search Indexes
|
||||
### 4.3 Full-Text Search Indexes
|
||||
```sql
|
||||
CREATE INDEX idx_ingredient_name_fts ON ingredient USING gin(to_tsvector('english', name));
|
||||
CREATE INDEX idx_ingredient_name_lower_fts ON ingredient USING gin(to_tsvector('english', name_lower));
|
||||
CREATE INDEX idx_recipe_name_fts ON recipe USING gin(to_tsvector('english', name));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Data Migration Strategy
|
||||
## 5. Day of Week Convention
|
||||
|
||||
### 4.1 Initial Schema
|
||||
Use SQLAlchemy or Alembic for schema management.
|
||||
**ISO-8601 standard** (1=Monday through 7=Sunday):
|
||||
- 1 = Monday
|
||||
- 2 = Tuesday
|
||||
- 3 = Wednesday
|
||||
- 4 = Thursday
|
||||
- 5 = Friday
|
||||
- 6 = Saturday
|
||||
- 7 = Sunday
|
||||
|
||||
### 4.2 Future Migrations
|
||||
- Alembic for version-controlled migrations
|
||||
- All migrations must be reversible
|
||||
This applies to `meal_plan_item.day_of_week`.
|
||||
|
||||
### 4.3 Seed Data
|
||||
- Basic ingredient list pre-populated
|
||||
- Sample recipes for initial testing (5-10 meals)
|
||||
- Default family profile template
|
||||
**Note**: PostgreSQL's `EXTRACT(DOW FROM date)` returns 0=Sunday, 6=Saturday. Convert accordingly.
|
||||
|
||||
---
|
||||
|
||||
## 5. Data Retention
|
||||
## 6. Data Migration Strategy
|
||||
|
||||
### 6.1 Initial Schema
|
||||
Use Alembic for version-controlled migrations.
|
||||
|
||||
### 6.2 Migration Policy
|
||||
- All schema changes must go through Alembic migrations
|
||||
- Migrations must be reversible where possible
|
||||
- No `Base.metadata.create_all()` in application startup code
|
||||
|
||||
### 6.3 Seed Data
|
||||
- Enum types created first
|
||||
- Basic ingredients pre-populated (50-100 common items)
|
||||
- Sample recipes for initial testing (5-10 meals)
|
||||
- Default family profile with 2 adult members
|
||||
|
||||
---
|
||||
|
||||
## 7. 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 |
|
||||
| Used approval tokens | 7 days | Delete after vote processed |
|
||||
| Expired approval tokens | 30 days | Delete |
|
||||
|
||||
---
|
||||
|
||||
## 6. Row-Level Security (Future)
|
||||
## 8. Row-Level Security (Future)
|
||||
|
||||
If multi-family support is added:
|
||||
- RLS on all tables
|
||||
- Policies based on `family_profile_id`
|
||||
- Backend enforces tenant isolation
|
||||
- Current implementation: single-family, no RLS needed
|
||||
|
||||
Reference in New Issue
Block a user