# Architecture Patterns: Household Food Inventory Apps **Domain:** Multi-user household inventory management **Researched:** 2026-01-27 **Confidence Level:** MEDIUM-HIGH ## Executive Summary Household food inventory apps require careful separation of concerns across multi-user households, real-time synchronization, offline-first resilience, and barcode/product lookup. The recommended architecture uses a **tenant-per-household model with PostgreSQL multi-tenancy**, Supabase Realtime for synchronization, client-side barcode scanning with cached product lookup, and local-first conflict resolution with server arbitration. This document defines component responsibilities, data flow, build order implications, and scaling thresholds. --- ## Recommended Architecture: System Overview ``` ┌─────────────────────────────────────────────────────────────────┐ │ FLUTTER MOBILE APP │ │ (iOS/Android - runs on 2-10 household members' phones) │ │ │ │ ┌──────────────┐ ┌─────────────┐ ┌──────────────────┐ │ │ │ Barcode │ │ Local │ │ UI/Sync │ │ │ │ Scanner │ │ Cache/DB │ │ Controllers │ │ │ │ (ML Kit) │ │ (SQLite) │ │ (Riverpod) │ │ │ └──────────────┘ └─────────────┘ └──────────────────┘ │ │ │ │ │ │ │ └───────────────────┼──────────────────┘ │ │ │ │ │ Queue of local │ Periodic sync (5-30s) │ │ changes before │ via websocket │ │ connection back │ │ └─────────────────────────────────────────────────────────────────┘ │ HTTPS + WSS (auth token, household_id) │ ┌─────────────────────────────────────────────────────────────────┐ │ SUPABASE/BACKEND │ │ (Postgres + Realtime + Auth + Storage) │ │ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ API Layer (PostgREST) │ │ │ │ - REST endpoints for CRUD │ │ │ │ - Row-Level Security (RLS) enforces household isolation│ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌──────────────────────────┼──────────────────────────────┐ │ │ │ │ │ │ │ │ ┌──────────────────┐ ┌──────────────────┐ │ │ │ │ │ Postgres DB │ │ Realtime │ │ │ │ │ │ (households, │ │ Broadcast │ │ │ │ │ │ items, │ ◄─┤ (websocket) │ │ │ │ │ │ transactions) │ │ - subscriptions │ │ │ │ │ │ │ │ - broadcasts │ │ │ │ │ └──────────────────┘ └──────────────────┘ │ │ │ │ │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ │ │ Business Logic (Edge Functions / HTTP API) │ │ │ │ │ │ - Conflict resolution │ │ │ │ │ │ - Notification triggers │ │ │ │ │ │ - Barcode lookup (fallback if not in local DB) │ │ │ │ │ │ - Prediction pipeline scheduling │ │ │ │ │ └──────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ │ │ Storage (S3-compatible) │ │ │ │ │ │ - Receipt images │ │ │ │ │ │ - Product photos (cached from barcode API) │ │ │ │ │ └──────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ ┌───────────┼───────────┐ │ │ │ ┌────────▼────┐ ┌───▼────┐ ┌──▼──────────┐ │ Barcode API │ │Notif │ │ ML Pipeline │ │(Go-UPC, │ │Service │ │ (async job) │ │BarcodeLook) │ │(FCM, │ │ │ └─────────────┘ └────────┘ └─────────────┘ ``` --- ## Component Boundaries and Responsibilities ### 1. **Flutter Frontend (Mobile App)** **Responsibility:** Present UI, capture input, manage local state, handle offline operation. **Sub-components:** | Component | Responsibility | Technology | |-----------|--------------|-----------| | **Barcode Scanner Module** | Capture barcode images via camera, decode with ML Kit, query local cache | ML Kit (Google), `camera` plugin, `barcode_flutter` | | **Local SQLite Database** | Store inventory items, transactions, pending sync queue, barcode cache | SQLite via `sqflite` or `drift` | | **Realtime Listener** | Subscribe to household changes via Supabase Realtime, merge remote changes | `supabase_flutter`, websocket handling | | **Sync Engine** | Queue local changes, merge with remote state, handle conflicts, persist to server | Custom sync logic with exponential backoff | | **UI Controllers** | Inventory list, add item form, household members, notifications | Flutter widgets, Riverpod for state | | **Auth Manager** | Login, household selection, token refresh | `supabase_flutter` auth module | **Key Patterns:** - **Offline-First Local Storage:** All writes happen to SQLite first, async sync to server. - **Pessimistic Locking for Critical Operations:** Changes to item quantities use server-side validation to prevent overselling (e.g., "remove 5 items but only 3 exist"). - **Periodic Full Sync:** Every 30-60s, fetch server version and merge (handles missed websocket messages). - **Notification Queue:** Local cache of notifications prevents duplicate alerts when reconnecting. **Scaling Implications:** - At 1000+ items in a household, SQLite performance degrades on full sync. Consider pagination. - Barcode cache should be limited to 10K most-recent scans per household (local storage constraint: ~200MB). --- ### 2. **Supabase Backend: PostgreSQL Database** **Responsibility:** Single source of truth, enforce consistency, isolate households, track history. **Schema Overview:** ```sql -- Households (tenants) CREATE TABLE households ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, created_at TIMESTAMP DEFAULT now(), created_by UUID REFERENCES auth.users(id) ); -- Household members (authorization) CREATE TABLE household_members ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), household_id UUID NOT NULL REFERENCES households(id), user_id UUID NOT NULL REFERENCES auth.users(id), role TEXT CHECK (role IN ('owner', 'member', 'guest')), joined_at TIMESTAMP DEFAULT now(), UNIQUE(household_id, user_id) ); -- Inventory items (what's in the fridge) CREATE TABLE inventory_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), household_id UUID NOT NULL REFERENCES households(id), -- Product info (either from barcode lookup or manual entry) name TEXT NOT NULL, barcode TEXT, category TEXT, quantity DECIMAL(10, 2) NOT NULL DEFAULT 0, unit TEXT DEFAULT 'count', -- Expiration tracking expiration_date DATE, purchase_date DATE, purchase_price DECIMAL(10, 2), -- Sync metadata created_by UUID NOT NULL REFERENCES auth.users(id), created_at TIMESTAMP DEFAULT now(), updated_at TIMESTAMP DEFAULT now(), -- For conflict resolution last_modified_by UUID REFERENCES auth.users(id), version_vector TEXT, -- serialized clock for CRDT (optional) UNIQUE(household_id, barcode) -- prevent duplicates ); -- Transaction log (audit trail + sales data) CREATE TABLE transactions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), household_id UUID NOT NULL REFERENCES households(id), item_id UUID REFERENCES inventory_items(id), type TEXT CHECK (type IN ('add', 'remove', 'consume', 'discard')), quantity_change DECIMAL(10, 2), notes TEXT, -- "found at Costco $12.99", "expired" receipt_image_path TEXT REFERENCES storage.objects(id), created_by UUID NOT NULL REFERENCES auth.users(id), created_at TIMESTAMP DEFAULT now(), -- For community sales data store_name TEXT, sale_price DECIMAL(10, 2), confidence_score DECIMAL(3, 2) -- 0-1: trust level for crowd-sourced prices ); -- Barcode lookup cache (local cache of product info) CREATE TABLE barcode_cache ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), barcode TEXT UNIQUE NOT NULL, product_name TEXT, product_category TEXT, image_url TEXT, expiration_estimate_days INTEGER, -- Metadata source TEXT, -- 'go_upc', 'user_submitted', 'crowdsourced' last_updated TIMESTAMP DEFAULT now(), lookup_count INTEGER DEFAULT 0 -- track popularity ); -- Notifications (generated by triggers) CREATE TABLE notifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), household_id UUID NOT NULL REFERENCES households(id), user_id UUID NOT NULL REFERENCES auth.users(id), type TEXT CHECK (type IN ( 'item_expiring_soon', 'item_expired', 'low_stock', 'member_added_item', 'household_shared', 'sale_alert' )), item_id UUID REFERENCES inventory_items(id), title TEXT, message TEXT, read_at TIMESTAMP, created_at TIMESTAMP DEFAULT now(), expires_at TIMESTAMP, -- auto-delete old notifications -- Delivery status delivered_via TEXT, -- 'in_app', 'push', 'email' delivery_status TEXT DEFAULT 'pending' -- 'delivered', 'failed' ); -- Household settings & preferences CREATE TABLE household_settings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), household_id UUID UNIQUE NOT NULL REFERENCES households(id), -- Notification preferences notify_expiry_days INTEGER DEFAULT 3, notify_low_stock_threshold DECIMAL(10, 2), enable_push_notifications BOOLEAN DEFAULT true, enable_email_summary BOOLEAN DEFAULT false, -- Privacy & sharing share_sales_data BOOLEAN DEFAULT false, allow_community_lookup BOOLEAN DEFAULT true, updated_at TIMESTAMP DEFAULT now() ); -- Enable Row-Level Security ALTER TABLE households ENABLE ROW LEVEL SECURITY; ALTER TABLE inventory_items ENABLE ROW LEVEL SECURITY; ALTER TABLE transactions ENABLE ROW LEVEL SECURITY; ALTER TABLE notifications ENABLE ROW LEVEL SECURITY; -- RLS Policies: Users can only see their own households CREATE POLICY households_rls ON households FOR SELECT USING ( EXISTS ( SELECT 1 FROM household_members WHERE household_id = households.id AND user_id = auth.uid() ) ); CREATE POLICY items_rls ON inventory_items FOR SELECT USING ( EXISTS ( SELECT 1 FROM household_members WHERE household_id = inventory_items.household_id AND user_id = auth.uid() ) ); ``` **Key Features:** - **Household Isolation via RLS:** Row-Level Security policies ensure users can only access their household's data. - **Version Vector (CRDT-ready):** The `version_vector` column stores logical clocks for eventual consistency (not required for MVP, but supports offline-first at scale). - **Transaction Log for Audit & Community Data:** Every change is logged, enabling replay, sales data aggregation, and fraud detection. - **Notification Triggers:** PostgreSQL triggers or Edge Functions auto-generate notifications on state changes. --- ### 3. **Realtime Sync Layer (Supabase Realtime)** **Responsibility:** Push changes from server to clients in real-time via websockets. **Patterns:** | Pattern | How It Works | When to Use | |---------|------------|-----------| | **Postgres Changes** | Subscribe to table changes (INSERT, UPDATE, DELETE). Server broadcasts to all subscribers. | Default for inventory items, transactions | | **Broadcast** | Ephemeral messages for non-persistent state (typing indicators, user presence) | Optional: "User X is editing this item" | | **Presence** | Track which users are currently online in a household | Nice-to-have: show "3 members active now" | **Architecture:** ``` [Client A] --websocket--> [Realtime Server] [Client B] --websocket--> [Realtime Server] --postgres trigger--> [Postgres] [Client C] --websocket--> [Realtime Server] When item added by Client A: 1. Client A sends REST POST /inventory_items 2. Postgres insert triggers NOTIFY on 'inventory_items' channel 3. Realtime server broadcasts to all clients subscribed to that household 4. Clients B & C update local SQLite and UI in <500ms ``` **Limitations (Medium confidence from Supabase docs):** - Realtime has connection limits per project (check Supabase pricing tier). - If a client misses a message (network blip), no automatic replay. - **Solution:** Periodic full-sync every 30-60s acts as a catch-up mechanism. --- ### 4. **Barcode Scanning & Product Lookup** **Responsibility:** Decode barcodes, find product info, cache results. **Architecture:** ``` Barcode Scan (on device) │ ├─> ML Kit decodes barcode to UPC (instant, offline) │ └─> Query local cache (SQLite) │ ├─> Hit: Return product name, category, est. expiry │ └─> Miss: Query server cache │ ├─> Hit: Return & cache locally │ └─> Miss: Query external API (Go-UPC, BarcodeAPI) │ └─> Cache result + show "Add item" form ``` **Component Details:** | Layer | Technology | Responsibility | |-------|-----------|-----------------| | **On-Device Barcode Decode** | Google ML Kit (`google_ml_kit` Flutter plugin) | Fast, offline, works with UPC/EAN/QR codes | | **Local Cache (SQLite)** | `sqflite` with `barcode_cache` table | Last 10K lookups, keyed by UPC | | **Server-Side Cache** | Postgres `barcode_cache` table | Shared across household (saves API calls) | | **External Product DB** | Go-UPC, BarcodeAPI, EAN-Search (fallback) | Authoritative product data, images, nutrition info | **Decision Logic:** ```typescript async Future lookupBarcode(String upc) async { // 1. Try local cache (instant, offline) var local = await localDb.query('barcode_cache WHERE barcode = ?', [upc]); if (local.isNotEmpty) return ProductInfo.fromLocal(local[0]); // 2. Try server cache (household's previous lookups) if (hasNetwork) { var remote = await supabase .from('barcode_cache') .select() .eq('barcode', upc) .single(); if (remote != null) { await localDb.insert('barcode_cache', remote); // cache locally return ProductInfo.fromRemote(remote); } } // 3. Query external API (rate-limited, may fail) if (hasNetwork && apiCredits > 0) { var external = await goUpcApi.lookup(upc); // Cache on server for next household member await supabase.from('barcode_cache').insert({ barcode: upc, product_name: external.name, // ... other fields source: 'go_upc', }); await localDb.insert('barcode_cache', external); return ProductInfo.fromExternal(external); } // 4. Give up, let user enter manually return null; } ``` **Scaling Implications:** - External API calls cost money ($0.01-0.05 per lookup). Cache aggressively. - At 10K households with 50 items each (500K items), barcode API would cost $5K-25K/month if not cached. - **Solution:** Server-side cache reduces API calls by 80-90% (most households share products). --- ### 5. **Offline Sync & Conflict Resolution** **Responsibility:** Queue local changes, sync when online, resolve conflicts. **Offline-First Sync Flow:** ``` User is offline: 1. Add item "Milk" to inventory └> Write to local SQLite immediately - Mark as `synced=false, pending_id=uuid1` 2. User takes multiple actions (add bread, remove eggs, update quantity) └> All queued in local `pending_changes` table User comes back online: 3. App detects network (can reach backend) └> Sync begins with exponential backoff 4. For each pending change: a) Send to server: POST /inventory_items + pending_id b) Server applies change + returns new version c) If conflict: apply conflict resolution rule d) Update local record: mark synced=true, store server version 5. Receive broadcast of other members' changes └> Merge into local state (Last-Write-Wins by default) ``` **Conflict Resolution Strategies:** | Strategy | Pros | Cons | When to Use | |----------|------|------|-----------| | **Server Wins** | Simple, no lost updates | Local work discarded | Settings, shared state | | **Last-Write-Wins (LWW)** | Preserves data, deterministic | Requires accurate time sync, may lose recent edits | Quantity updates (last edit timestamp wins) | | **Custom: Unit + User** | Domain-aware | Complex to implement | Quantity: "If both edited, sum the changes" | | **CRDT** | No conflicts, eventual consistency | High complexity, storage overhead | Collaborative document editing (not MVP) | **Recommendation for Sage (MVP):** Use **Last-Write-Wins with server arbitration**: ```sql -- When conflict detected: -- Server has: item.quantity = 5, updated_at = 2026-01-27 10:00:00 -- Client submits: quantity = 3, with parent_version_id = old_version_id -- If parent version matches current, apply update UPDATE inventory_items SET quantity = 3, last_modified_by = auth.uid(), updated_at = now() WHERE id = item_id AND parent_version_id = submitted_parent_version_id; -- If conflict (parent mismatch), return conflict and let client choose: -- Option A: Accept server state (discard local change) -- Option B: Rebase local change on server state (re-apply user's delta) -- Option C: Manual merge (show user both versions) ``` --- ### 6. **Notification System** **Responsibility:** Trigger alerts for expiry, low stock, member activity; deliver via push/in-app/email. **Architecture:** ``` Event (item expires, low stock detected) │ ├─> PostgreSQL Trigger (realtime trigger) │ └─> INSERT notification row │ ├─> Supabase Realtime │ └─> Broadcast to subscribed clients │ └─> Edge Function (async job, on schedule) ├─> Query expiring items (SELECT * WHERE expiration_date <= today + 3 days) ├─> For each: generate notification if not already created └─> Send push to users (FCM for Android, APNs for iOS) ``` **Trigger-Based Notifications (Real-time):** ```sql -- Auto-generate notification when item expires CREATE OR REPLACE FUNCTION notify_item_expired() RETURNS TRIGGER AS $$ BEGIN IF NEW.expiration_date = CURRENT_DATE AND OLD.expiration_date != CURRENT_DATE THEN -- Create notification for all household members INSERT INTO notifications ( household_id, user_id, type, item_id, title, message ) SELECT NEW.household_id, hm.user_id, 'item_expired', NEW.id, 'Item Expired', NEW.name || ' has expired today' FROM household_members hm WHERE hm.household_id = NEW.household_id; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER item_expiry_trigger AFTER UPDATE ON inventory_items FOR EACH ROW EXECUTE FUNCTION notify_item_expired(); ``` **Scheduled Notifications (Daily/Periodic):** ```typescript // Supabase Edge Function (cron job) // Runs every day at 8am export async function handler(req: Request) { const supabase = createClient(DB_URL, DB_KEY); // Find items expiring in 0-3 days const { data: expiringItems } = await supabase .from('inventory_items') .select() .gte('expiration_date', today) .lte('expiration_date', today + 3 days); for (const item of expiringItems) { const household_id = item.household_id; // Check if notification already sent today const { data: existing } = await supabase .from('notifications') .select() .eq('item_id', item.id) .gte('created_at', today); if (existing.length === 0) { // Create notification await supabase.from('notifications').insert({ household_id, item_id: item.id, type: 'item_expiring_soon', title: 'Item Expiring Soon', message: `${item.name} expires in ${daysUntilExpiry} days`, }); // Send push to all household members await sendPushNotifications(household_id, { title: 'Item Expiring Soon', body: item.name, }); } } } ``` **Push Notification Delivery:** | Service | Responsibility | Configuration | |---------|--------------|---| | **FCM (Firebase Cloud Messaging)** | Deliver to Android devices | Requires Firebase project + server key | | **APNs (Apple Push Notification)** | Deliver to iOS devices | Requires Apple Developer account + certificate | | **In-App via Realtime** | Badge/banner inside app | Instant, no external service needed | | **Email (optional)** | Daily/weekly digest | SendGrid or Supabase email (future) | **Recommendation:** Start with **in-app only** (via Realtime broadcast), add push later when needed. --- ### 7. **Barcode Community Data Collection (Future)** **Responsibility:** Collect user-submitted barcode data safely, prevent spam. **Architecture:** ``` User scans item not in database │ └─> "Item not found. Help others by sharing details?" │ ├─> User enters: name, category, expiry estimate, store, price │ └─> Submit to server: POST /api/submit_product { barcode: "123456", product_name: "Organic Milk", category: "Dairy", expiration_estimate_days: 14, store_name: "Whole Foods", sale_price: 4.99 } ``` **Server-Side Processing:** ```typescript export async function handleProductSubmission(req: Request) { const { barcode, product_name, ... } = await req.json(); // 1. Validate input (length, format, etc.) if (!barcode || barcode.length < 8) { return error('Invalid barcode'); } // 2. Rate-limit per household (prevent spam) const { data: submissions } = await supabase .from('barcode_submissions') .select() .eq('household_id', user.household_id) .gte('created_at', now - 1 hour); if (submissions.length > 10) { return error('Too many submissions in last hour'); } // 3. Store with low confidence score (unverified) await supabase.from('barcode_cache').upsert({ barcode, product_name, source: 'user_submitted', confidence_score: 0.3, // Low confidence lookup_count: 0, }); // 4. Trigger review (queue for moderation) await supabase.from('barcode_submissions').insert({ barcode, household_id, submitter_id: auth.uid(), product_data: { product_name, category, ... }, status: 'pending_review', }); return { success: true, message: 'Thank you for helping!' }; } ``` **Trust Model:** - New submissions get `confidence_score = 0.3`. - If 3+ different households agree on same data, raise to `0.7`. - If data from external API, use `confidence_score = 0.95`. - Client app uses score to decide: show as suggestions vs. auto-fill. **Spam Prevention:** - Rate-limit: 10 submissions/hour per household. - Dedupe: Check if barcode already exists before storing. - Moderation: Flag suspicious submissions (gibberish, profanity). --- ### 8. **File Storage (Receipts, Product Images)** **Responsibility:** Store images with access control, optimize for mobile. **Architecture:** ``` User captures receipt photo │ ├─> Compress on device (resize to 1200x900 max) │ ├─> Upload to Supabase Storage │ POST /storage/v1/object/receipts/{household_id}/{uuid}.jpg │ Headers: Authorization: Bearer {token} │ └─> Storage RLS enforces household access: "Only household members can access household_id/* objects" ``` **Supabase Storage RLS Policy:** ```sql CREATE POLICY "household_can_access_own_receipts" ON storage.objects FOR SELECT USING ( bucket_id = 'receipts' AND (auth.uid() IN ( SELECT user_id FROM household_members WHERE household_id = split_part(name, '/', 1)::uuid )) ); ``` **File Organization:** ``` receipts/ ├── {household_id}/ │ ├── {uuid}.jpg -- receipt │ ├── {uuid}_thumb.jpg -- thumbnail for list view │ └── ... ``` **CDN & Image Optimization:** - Supabase Storage includes global CDN (285+ cities). - Built-in image transformation via `?width=400&quality=80` URL params. - Use transformed images in list views (faster load). **Costs:** - Storage: $5-25/month per 1TB. - Bandwidth: Included with Supabase plan (egress charges extra if >10GB/month). --- ### 9. **AI/ML Prediction Service (Future)** **Responsibility:** Estimate item expiry based on purchase date, category, storage conditions. **Recommended Architecture:** ``` User adds item "Fresh Strawberries" │ ├─> Client makes prediction request: │ POST /api/predict_expiry { │ product_name: "Fresh Strawberries", │ category: "Produce", │ purchase_date: "2026-01-27", │ storage_conditions: "refrigerator" │ } │ └─> Server-side ML: ├─> Load pre-trained model (small, ~5MB) │ (e.g., XGBoost or PyTorch mobile) │ ├─> Feature engineering: │ - Days since purchase │ - Category (produce: 3-7 days, dairy: 10-21 days) │ - Storage type (fridge extends shelf life 2-3x) │ - Historical data from transactions table │ ├─> Predict: "14 days expected shelf life" │ └─> Return confidence interval: [10, 18] days │ └─> Update item.expiration_date + confidence_score ``` **Model Location Options:** | Location | Pros | Cons | Recommendation | |----------|------|------|---| | **Server (Edge Function)** | Easy to update, centralized | Adds latency (100-300ms), requires network | **Default for MVP** | | **On-Device (TensorFlow Lite)** | Instant, works offline, privacy | Model size (5-20MB), requires device resources | Later versions | | **Hybrid** | Best of both | Complex to manage | Phase 2 optimization | **Data Requirements:** - Historical transactions with `type='consume'` to learn when users actually eat items. - At scale (10K+ transactions), model trains weekly to adapt to household preferences. --- ## Data Flow: Three Key Scenarios ### Scenario 1: User Adds Item Offline, Syncs Online ``` Timeline: [OFFLINE - no network] 10:00 User adds "Milk" via barcode scan ├─> Local lookup finds product in cache ├─> Write to local SQLite: inventory_items (synced=false) ├─> Write to pending_changes queue └─> Show "Syncing..." indicator [NETWORK RESTORED] 10:05 Sync engine detects network ├─> Dequeue pending_changes ├─> POST /inventory_items {item: {name, barcode, quantity, ...}} ├─> Server processes: INSERT into inventory_items ├─> Server broadcasts via Realtime: CHANNEL household:123:inventory └─> All clients receive update, merge into local DB 10:06 User's app receives Realtime broadcast ├─> Merge remote item into SQLite ├─> Update pending_changes: mark synced=true ├─> Mark local record with server_id └─> UI updates (no duplicate entry) Other household members: ├─> Receive Realtime broadcast ├─> "Dad added Milk" ├─> Notification generated: "John added Milk" └─> In-app notification + optional push ``` ### Scenario 2: Concurrent Edits (User A & B both remove from quantity) ``` State: item.quantity = 5 User A (offline): 10:00 ├─> Remove 2 units: quantity = 3 ├─> Write to local: {quantity: 3, version: A} ├─> Queue for sync User B (online): 10:01 ├─> Remove 1 unit: quantity = 4 (from current server state) ├─> POST /inventory_items/update (quantity=4, version=5) ├─> Server applies: quantity = 4, version = 6 User A (comes online): 10:05 ├─> Sync: POST /inventory_items/update {quantity=3, parent_version=5} ├─> Server: parent_version=5 exists, applies change ├─> Result: quantity = 3, version = 7 │ └─> Last-Write-Wins: A's edit (later timestamp) wins Server state: 3 units (A removed 2, B removed 1 earlier) ``` **NOTE:** This is lossy (B's edit partially lost). Better approach: ``` Use delta/operational transform: - User B: "subtract 1 from quantity" - User A: "subtract 2 from quantity" - Server merges both deltas: subtract 3 total - Result: quantity = 2 (both edits preserved) ``` This requires more sophisticated conflict resolution (CRDT-style). **Save for Phase 2.** ### Scenario 3: Barcode Not Found, User Contributes Data ``` User scans "012345678901" ├─> Local cache: miss ├─> Server cache: miss ├─> External API (go-upc): miss (rare barcode) ├─> UI shows: "Not found. Would you like to add it?" User confirms + enters: ├─> Name: "Artisanal Kombucha" ├─> Category: "Beverages" ├─> Expiry: "Keep refrigerated, 30 days" ├─> Store: "Whole Foods" ├─> Price: "$5.99" │ └─> POST /api/submit_product {barcode, ...} Server processes: ├─> Validate input + rate-limit check ├─> INSERT into barcode_cache (confidence_score=0.3, source='user_submitted') ├─> Queue for moderation review └─> Return: "Thank you! Data recorded." Next time ANY household member scans same barcode: ├─> Server cache hit (confidence=0.3) ├─> Show as suggestion: "Artisanal Kombucha?" with low-confidence indicator ├─> User can accept, edit, or ignore ``` --- ## Build Order Implications ### Phase 1: MVP Foundations (Weeks 1-4) **Builds these first:** 1. **Flutter App Scaffolding** + local SQLite - Barcode scanning (ML Kit integration) - Local cache layer - Offline-capable UI 2. **Supabase Backend** - Households + members table - Inventory_items table with RLS - Authentication (email/password) 3. **Basic Sync** - REST API (add/remove/edit items) - Realtime listeners (websocket to SQLite) - Simple Last-Write-Wins conflict resolution **Test:** Single household, 2 users, add/remove items, offline → online sync. --- ### Phase 2: Polish + Scale (Weeks 5-8) **Builds on Phase 1:** 4. **Barcode Lookup Service** - Server-side cache integration - External API fallback (Go-UPC) - Local caching strategy 5. **Notification System** - Expiry alerts (trigger-based) - Low-stock alerts (periodic) - In-app notifications (Realtime broadcast) 6. **File Storage** - Receipt image uploads - Supabase Storage RLS **Test:** Full inventory workflow (scan → add → receive alerts → sync across 3+ devices). --- ### Phase 3: Community + Intelligence (Weeks 9-12) **Builds on Phases 1-2:** 7. **Community Barcode Data** - User submissions - Spam prevention - Confidence scoring 8. **Prediction Service** - Server-side ML (Edge Function) - Expiry estimation - Historical learning 9. **Sales Data Collection** - Aggregate community prices - Query for price alerts **Test:** Households contribute data, predictions improve over time. --- ## Scalability Thresholds ### At 100 Households (~500 users, 5K items) | Component | Threshold | Mitigation | |-----------|-----------|-----------| | **SQLite local DB** | ~500 items | Partition by household, use indexes | | **Supabase Realtime** | <100 concurrent connections | Realtime works fine at this scale | | **Postgres** | <1M rows total | Standard PostgreSQL handles easily | | **Barcode cache hits** | 70-80% | Most households share products | | **External API calls** | ~500/day | Budget: <$10/month | **Status:** ✅ No changes needed, standard patterns work. --- ### At 10K Households (~50K users, 500K items) | Component | Threshold | Mitigation | |-----------|-----------|-----------| | **Postgres queries** | Queries slow (>500ms) | Add indexes on (household_id, barcode) | | **Realtime connections** | May hit limits (check Supabase tier) | Consider connection pooling or batching | | **Barcode cache** | 1M+ cache entries | Archive old entries (unused >90 days) | | **File storage** | ~10TB images | Implement auto-cleanup (delete old receipts) | | **Notification generation** | 500K expiry checks/day | Move from triggers to batch job (Edge Function) | | **External API costs** | ~5K calls/day | At 90% cache hit: ~500 calls/day (~$5-25/month) | **Status:** ⚠️ Some optimization needed: 1. **Add database indexes:** ```sql CREATE INDEX idx_items_household_barcode ON inventory_items(household_id, barcode); CREATE INDEX idx_items_expiration ON inventory_items(household_id, expiration_date); CREATE INDEX idx_transactions_household ON transactions(household_id, created_at); ``` 2. **Batch notification generation** (not trigger-based): ```typescript // Run once daily via cron (Edge Function) // SELECT COUNT(*) per day << triggers on EVERY UPDATE ``` 3. **Archive old barcode cache:** ```sql -- Monthly job: delete unused barcodes DELETE FROM barcode_cache WHERE lookup_count = 0 AND last_updated < now - 90 days; ``` --- ### At 100K+ Households (Beyond MVP) | Component | Threshold | Mitigation | |-----------|-----------|-----------| | **Postgres single instance** | ~10M rows, reaching limits | Read replicas + connection pooling | | **Realtime server capacity** | 10K+ concurrent connections | May require dedicated Realtime cluster | | **Storage costs** | ~100TB | Move to S3 with Supabase S3 integration | | **Data replication** | Async sync issues | Consider CRDT or event-sourced architecture | **Status:** ❌ Significant re-architecture needed. Out of scope for MVP/Phase 1-2. --- ## Self-Hosted Deployment Strategy ### Docker Compose Stack (for self-hosting) If deploying on-premises instead of Supabase cloud: ```yaml version: '3.8' services: # PostgreSQL database postgres: image: postgres:15 environment: POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data ports: - "5432:5432" # Supabase API (PostgREST) postgrest: image: postgrest/postgrest:v11 environment: PGRST_DB_URI: postgres://user:password@postgres:5432/sage PGRST_JWT_SECRET: ${JWT_SECRET} ports: - "3000:3000" depends_on: - postgres # Supabase Realtime (WebSocket server) realtime: image: supabase/realtime:latest environment: DATABASE_URL: postgres://user:password@postgres:5432/sage ports: - "4000:4000" depends_on: - postgres # Auth service (Supabase Auth) auth: image: supabase/auth:v2 environment: POSTGRES_PASSWORD: ${DB_PASSWORD} JWT_SECRET: ${JWT_SECRET} SITE_URL: https://yourdomain.com ADDITIONAL_REDIRECT_URLS: https://yourdomain.com/callback ports: - "9999:9999" depends_on: - postgres # Storage (S3-compatible, e.g., MinIO) storage: image: minio/minio:latest environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: ${MINIO_PASSWORD} volumes: - minio_data:/minio_data ports: - "9000:9000" command: minio server /minio_data # Nginx reverse proxy nginx: image: nginx:latest volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro ports: - "80:80" - "443:443" depends_on: - postgrest - realtime - auth volumes: postgres_data: minio_data: ``` **Deployment Notes:** - **Infrastructure:** VPS (4GB RAM, 20GB disk minimum) costs $10-30/month. - **Complexity:** Self-hosting requires DevOps expertise (backups, monitoring, updates). - **Recommendation:** Start with Supabase cloud (free tier), migrate to self-hosted if cost becomes issue. --- ## Anti-Patterns to Avoid ### 1. **Syncing Every Field Change Individually** ❌ **Bad:** ```typescript // Don't do this: 5 separate HTTP requests await api.updateQuantity(item_id, 5); await api.updateExpiry(item_id, '2026-02-01'); await api.updateNotes(item_id, 'From Costco'); await api.updatePrice(item_id, 12.99); await api.updateCategory(item_id, 'Dairy'); ``` ✅ **Good:** ```typescript // Single request with all changes await api.updateItem(item_id, { quantity: 5, expiration_date: '2026-02-01', notes: 'From Costco', purchase_price: 12.99, category: 'Dairy' }); ``` **Why:** Reduces network overhead, reduces merge conflicts, faster sync. --- ### 2. **Ignoring Offline Scenarios** ❌ **Bad:** ```typescript // Assumes network always available async addItem(item) { await api.post('/items', item); notifyUser('Item added'); } ``` ✅ **Good:** ```typescript // Works offline or online async addItem(item) { // Write locally first await localDb.insert(item); notifyUser('Item added'); // Sync in background if (hasNetwork) { try { const response = await api.post('/items', item); await localDb.update(item.id, { server_id: response.id, synced: true }); } catch (err) { // Will retry next time online } } } ``` **Why:** Mobile networks are unreliable, users expect offline functionality. --- ### 3. **Forgot WHERE Clause (RLS Bypass)** ❌ **Critical Security Bug:** ```typescript // What if RLS is disabled by accident? const items = await supabase .from('inventory_items') .select(); // Returns ALL items, all households! ``` ✅ **Good:** ```typescript // RLS enforces this, but be explicit anyway const items = await supabase .from('inventory_items') .select() .eq('household_id', user.current_household_id); ``` **Why:** Defense in depth. Assume RLS has bugs, code defensively. --- ### 4. **Barcode API Without Caching** ❌ **Bad (expensive):** ```typescript // Every lookup hits external API async scanBarcode(upc) { return await goUpcApi.lookup(upc); // $0.01 per call } ``` ✅ **Good:** ```typescript // 3-tier caching async scanBarcode(upc) { // Local first let product = await localCache.get(upc); if (product) return product; // Server second (shared across household) product = await serverCache.get(upc); if (product) { await localCache.set(upc, product); return product; } // External only if miss product = await externalApi.lookup(upc); await serverCache.set(upc, product); await localCache.set(upc, product); return product; } ``` **Why:** External API calls are expensive and slow. 90% of scans are repeats. --- ### 5. **No Expiration Policy on Notifications** ❌ **Bad (data bloat):** ```sql -- Notification table grows forever SELECT COUNT(*) FROM notifications; -- Returns: 50 million rows after 1 year ``` ✅ **Good:** ```sql -- Auto-delete old notifications CREATE TRIGGER cleanup_old_notifications AFTER INSERT ON notifications FOR EACH STATEMENT EXECUTE FUNCTION delete_old_notifications(); CREATE FUNCTION delete_old_notifications() RETURNS void AS $$ DELETE FROM notifications WHERE created_at < NOW() - INTERVAL '30 days'; $$ LANGUAGE SQL; ``` **Why:** Notifications are transient, archiving them indefinitely wastes storage. --- ### 6. **Conflict Resolution Without Version Tracking** ❌ **Bad:** ```typescript // Two clients update same field, no way to detect conflict user_a.item.quantity = 5; user_b.item.quantity = 5; // Both send updates, last write wins, no awareness of conflict ``` ✅ **Good:** ```typescript // Track version numbers or timestamps { item_id: "abc123", quantity: 5, version: 10, // Server increments this updated_at: "2026-01-27T10:00:00Z" } // Client includes version in update request PUT /items/abc123 { quantity: 4, version: 10 } // Server checks: "Is version still 10?" // If yes: apply update, increment version to 11 // If no: conflict! Return conflict response ``` **Why:** Without versions, conflicts are silent. Users lose data unknowingly. --- ## Architecture Checklist for Sage - [ ] **Authentication:** Implement household-aware auth (user can be in multiple households, select one per session) - [ ] **RLS Policies:** Test that users cannot access other households' data - [ ] **Barcode Scanning:** Integrate ML Kit on device, implement 3-tier cache - [ ] **Realtime Sync:** Test websocket reconnection, missed message catch-up - [ ] **Offline Queue:** Persist pending changes to SQLite, retry on network - [ ] **Conflict Resolution:** Implement Last-Write-Wins as default, log conflicts for analysis - [ ] **Notifications:** Trigger-based for real-time events, scheduled for periodic checks - [ ] **File Storage:** Set up Supabase Storage with RLS, implement image compression - [ ] **Error Handling:** Graceful degradation when API fails (show cached data, queue for later) - [ ] **Monitoring:** Log sync errors, API failures, conflict rates to detect issues --- ## Sources - [Supabase Realtime Documentation](https://supabase.com/docs/guides/realtime) - [Listening to Postgres Changes with Flutter](https://supabase.com/docs/guides/realtime/realtime-listening-flutter) - [Supabase Storage Documentation](https://supabase.com/docs/guides/storage) - [Multi-Tenant Database Architecture Patterns](https://www.bytebase.com/blog/multi-tenant-database-architecture-patterns-explained/) - [Designing Postgres Database for Multi-tenancy](https://www.crunchydata.com/blog/designing-your-postgres-database-for-multi-tenancy) - [Offline-First App Architecture](https://docs.flutter.dev/app-architecture/design-patterns/offline-first) - [Three Approaches to Offline-First Development](https://academy.realm.io/posts/three-approaches-offline-first-development/) - [Barcode Scanning Best Practices](https://www.scandit.com/blog/make-barcode-scanner-app-performant/) - [Go-UPC Barcode Database](https://go-upc.com/) - [Barcode Lookup API](https://www.barcodelookup.com/api) - [Conflict-Free Replicated Data Types (CRDTs)](https://crdt.tech/) - [Last-Write-Wins vs CRDTs](https://dzone.com/articles/conflict-resolution-using-last-write-wins-vs-crdts) - [Self-Hosting Supabase with Docker](https://supabase.com/docs/guides/self-hosting/docker) - [Notification System Design Best Practices](https://www.systemdesignhandbook.com/guides/design-a-notification-system/) - [Push vs In-App Notifications 2026](https://www.pushengage.com/push-vs-in-app-notifications/) - [Mobile App Image Optimization](https://www.scandit.com/blog/make-barcode-scanner-app-performant/) - [Food Expiration Prediction ML](https://www.sciencedirect.com/science/article/abs/pii/S0924224425001256) - [Artificial Intelligence for Food Safety](https://www.sciencedirect.com/science/article/pii/S0924224425002894)