# Technology Stack **Project:** Sage (Household Food Inventory Tracker) **Researched:** 2026-01-27 **Focus:** Completely free, open source, self-hostable with Supabase free tier + optional Docker ## Executive Summary Sage's tech stack prioritizes cost-free operation, multi-platform reach, and self-hosting capability. Core recommendations: **Supabase** (free tier PostgreSQL + Realtime) as primary backend with optional self-hosted Docker deployment, **Flutter + Riverpod 3.0** for shared mobile/web code, **mobile_scanner** for barcode capture, **Open Food Facts API** for product data (with local caching), and **Hive** for offline inventory caching. This combination keeps both development and operational costs at $0 while providing production-quality infrastructure. --- ## Recommended Stack ### Frontend: Flutter (Android + Web) | Technology | Version | Purpose | Why | |------------|---------|---------|-----| | **Flutter** | 3.24+ | Cross-platform UI framework | Single codebase for Android, iOS, and web. Shared code reduces maintenance burden. Production-ready, widely adopted. | | **Riverpod 3.0+** | 3.2.0+ (pub.dev) | State management | Compile-time safety, automatic retry on network errors, lowest boilerplate. New Mutations API simplifies write operations (critical for inventory updates). Better than Provider/BLoC for this domain. | | **mobile_scanner 7.1.4+** | 7.1.4 | Barcode scanning | Lightweight, supports Android/iOS/Web. Uses ML Kit on Android, Vision Kit on iOS, ZXing on web. No dependency on paid SDKs. Supports EAN-13, UPC-A, Code 128 (via underlying ML Kit/VisionKit/ZXing). | | **Hive 4.0+** | Latest | Local offline cache | Fast encrypted NoSQL storage (~500ms reads vs 8000ms SharedPreferences). Essential for offline inventory viewing when synced from server. AES-256 encryption built-in. | | **go_router 12+** | Latest | Navigation | Type-safe routing for multi-screen app. Works on all platforms. | #### Frontend Architecture ``` lib/ ├── main.dart ├── providers/ # Riverpod state management │ ├── auth_provider.dart │ ├── inventory_provider.dart # FutureProvider wraps Supabase calls │ ├── barcode_provider.dart # Caches barcode lookups │ └── household_provider.dart # Multi-user household context ├── screens/ # UI layer │ ├── inventory/ │ ├── barcode_scan/ │ ├── expiry_alerts/ │ └── shopping/ ├── services/ # Business logic │ ├── supabase_client.dart # Supabase initialization │ ├── barcode_service.dart # Barcode scanning + caching │ ├── open_food_facts_service.dart │ └── notification_service.dart └── models/ # Data classes ├── food_item.dart ├── household.dart └── user.dart ``` **State Management Pattern:** - Riverpod `FutureProvider` wraps Supabase calls (read-only) - Riverpod `Notifier` + `NotifierProvider` for mutations (inventory updates, deletions) - Hive for offline cache of recently-viewed inventory - Automatic sync when connection restored via Riverpod's automatic retry ### Backend: Supabase (Hosted Free + Optional Self-Hosted) | Technology | Version | Purpose | Why | |------------|---------|---------|-----| | **Supabase** | Latest | Backend-as-a-Service | PostgreSQL (not Firestore) = full relational power for complex queries. Free tier supports 50K MAU and unlimited API calls (critical). Realtime subscriptions included free tier (10K concurrent connections). No per-request billing like Firebase. Self-hosted Docker option available. | | **PostgreSQL 15+** | Managed by Supabase | Relational database | SQL allows complex joins (households, users, inventory, expiry tracking). ACID transactions for multi-user safety. Row-level security (RLS) built into Supabase. Extensible with triggers/functions. | | **Supabase Realtime** | Built-in | Multi-device sync | PostgreSQL logical replication → WebSocket broadcasts. Included free. 200 concurrent peak connections on free tier (sufficient for small households). Clients subscribe to table changes. | | **Supabase Auth** | Built-in | User authentication | Email/password + OAuth ready. Row-level security policies tied to auth context. 50K MAU free. | | **Supabase Storage** | Built-in | File uploads | 1 GB free. Use for food photos (optional UI feature later). | **Free Tier Constraints (2026):** - 500 MB database storage (sufficient for 10K+ food items with metadata) - 2 GB database egress per month (watch for high-volume syncs) - 200 concurrent realtime connections peak (one connection per client = 200 simultaneous users) - **CRITICAL GOTCHA:** Free projects pause after 7 days inactivity. **Mitigation:** Add keep-alive job (GitHub Actions pings API weekly) or self-host. Users cannot avoid this on hosted free tier. - Limited to 2 active projects (paused projects don't count) #### Self-Hosted Option (Docker Compose) For users wanting to avoid inactivity pausing: ```bash # Clone Supabase repo git clone https://github.com/supabase/supabase.git cd supabase/docker # Configure .env with secure keys (NEVER use defaults) cp .env.example .env # Edit .env: Change ANON_KEY, SERVICE_ROLE_KEY, JWT_SECRET # Start services docker-compose up -d # Minimum requirements: 8GB RAM, 25GB SSD # Remove unused services from docker-compose.yml if needed: # - Logflare (analytics) - unnecessary # - imgproxy (image resizing) - unnecessary early # Keep: postgres, kong (API gateway), auth, realtime, storage ``` **Self-hosted costs:** $0/month software + infrastructure (5-30 EUR/month for basic VPS) ### Database Schema (PostgreSQL) ```sql -- Multi-tenant: Shared database, shared schema, tenant_id discriminator -- Simplest approach for self-hosted, scales to thousands of households CREATE TABLE households ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, created_by UUID REFERENCES auth.users(id), created_at TIMESTAMP DEFAULT NOW() ); CREATE TABLE household_members ( household_id UUID REFERENCES households(id) ON DELETE CASCADE, user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, role TEXT DEFAULT 'member', -- 'owner' or 'member' joined_at TIMESTAMP DEFAULT NOW(), PRIMARY KEY (household_id, user_id) ); CREATE TABLE food_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), household_id UUID NOT NULL REFERENCES households(id) ON DELETE CASCADE, name TEXT NOT NULL, category TEXT, -- 'produce', 'dairy', 'meat', 'pantry', etc. barcode TEXT, -- EAN-13, UPC-A cached from Open Food Facts quantity INT DEFAULT 1, unit TEXT, -- 'pieces', 'kg', 'liters', 'ml' expiry_date DATE, location TEXT, -- 'fridge', 'freezer', 'pantry' purchase_date DATE, product_data JSONB, -- From Open Food Facts (name, brand, nutrition, ingredients) added_by UUID REFERENCES auth.users(id), created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX idx_food_household_expiry ON food_items(household_id, expiry_date); -- Barcode -> Product cache (avoids repeated API calls) CREATE TABLE barcode_cache ( barcode TEXT PRIMARY KEY, product_data JSONB, -- Full OFF product info cached_at TIMESTAMP DEFAULT NOW(), source TEXT -- 'open_food_facts', 'barcode_lookup' ); -- Expiry alerts log (for notification dedup) CREATE TABLE expiry_alerts_sent ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), household_id UUID NOT NULL, food_item_id UUID NOT NULL, alert_type TEXT, -- 'expiring_soon', 'expired' sent_at TIMESTAMP DEFAULT NOW(), UNIQUE(food_item_id, alert_type, DATE(sent_at)) ); -- Usage history (for shopping list AI predictor) CREATE TABLE consumption_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), household_id UUID NOT NULL, food_name TEXT NOT NULL, category TEXT, quantity_consumed INT, consumed_date DATE NOT NULL, household_id UUID REFERENCES households(id) ON DELETE CASCADE, created_at TIMESTAMP DEFAULT NOW() ); -- Row-Level Security: Each household only sees their data ALTER TABLE food_items ENABLE ROW LEVEL SECURITY; CREATE POLICY "Users can only access their household's items" ON food_items FOR SELECT USING ( household_id IN ( SELECT household_id FROM household_members WHERE user_id = auth.uid() ) ); CREATE POLICY "Users can modify their household's items" ON food_items FOR UPDATE USING ( household_id IN ( SELECT household_id FROM household_members WHERE user_id = auth.uid() ) ); ``` ### Data Sync Architecture **Realtime sync (Supabase Realtime):** ```dart // Riverpod provider subscribes to food_items table changes final inventoryProvider = StreamProvider>((ref) { final userId = ref.watch(authProvider).value?.id; if (userId == null) return const Stream.empty(); return supabase .from('food_items') .stream(primaryKey: ['id']) .eq('household_id', currentHouseholdId) // RLS enforces this .map((List data) => (data as List).map((item) => FoodItem.fromJson(item)).toList() ); }); // UI rebuilds automatically when server sends changes // Other family members' updates appear in real-time ``` ### Barcode Scanning & Product Lookup | Technology | Version | Purpose | Why | |------------|---------|---------|-----| | **mobile_scanner** | 7.1.4+ | Barcode capture | Open source, ML Kit backend, no API costs | | **Open Food Facts API** | Public | Product database | Free, no auth required. 100 req/min for product lookup. Community-driven. Best nutritional data coverage. | | **Barcode Lookup / EAN-Search (fallback)** | Free tier | Secondary product source | If OFF doesn't have product. Free lookup available. | #### Barcode Caching Strategy **Problem:** Open Food Facts rate limits = 100 req/min for product queries. With household sync, this can be hit quickly if multiple users scan same barcode. **Solution: Client-side + Server-side cache** ```dart // Frontend: Hive local cache (checked first) final barcodeService = BarcodeService(); final cachedProduct = hiveBox.get(barcode); if (cachedProduct != null && !isStale(cachedProduct)) { return cachedProduct; // 0ms, no API call } // Server cache: PostgreSQL barcode_cache table (checked second) final serverCached = await supabase .from('barcode_cache') .select() .eq('barcode', barcode) .single(); if (serverCached.exists && isRecent(serverCached)) { return serverCached; // Store in Hive for next time } // Hit Open Food Facts API (last resort) final product = await openFoodFactsApi.getProduct(barcode); // Store in both Hive + PostgreSQL await hiveBox.put(barcode, product); await supabase.from('barcode_cache').insert({ 'barcode': barcode, 'product_data': product.toJson(), 'source': 'open_food_facts' }); return product; ``` **Cache invalidation:** 30-day TTL per barcode (products rarely change that fast). Manual refresh available in UI. ### Notifications & Integrations | Technology | Version | Purpose | Why | |------------|---------|---------|-----| | **Supabase Edge Functions** | Built-in | Scheduled jobs | Serverless functions (TypeScript) check expiry dates nightly, trigger alerts. Free tier: 500K invocations/month. | | **ntfy.sh** | Self-hosted or public | Push notifications | Open source, free. Send to ntfy topics or bridge to Discord/Slack. No infrastructure needed for basic version. | | **Discord Webhooks** | Native | Optional alert channel | Teams can subscribe household alerts to Discord channel. 0 cost. | **Expiry Alert Flow:** ``` 1. Nightly Edge Function (11 PM) → SELECT food_items WHERE expiry_date BETWEEN TODAY AND TODAY+3 DAYS → Grouped by household_id 2. For each household: → Check dedup table (already alerted today for this item?) → If new: send via ntfy.sh OR Discord webhook if configured → Log to expiry_alerts_sent table 3. Client receives alert: → Notification badge on app → If ntfy: push to phone → If Discord: posted to channel ``` ### Simple AI/ML for Shopping Prediction | Technology | Purpose | Implementation | |------------|---------|-----------------| | **Simple statistical approach** | Predict next purchase date | No heavy ML needed. Use `consumption_history` table. When user marks item consumed, log to table. Riverpod provider computes: frequency = items consumed per month. Next purchase = last_purchase + (30 / frequency). Display on shopping list as "Usually buy X every Y days". | | **Hive (if expanding)** | Store prediction model coefficients | Not needed initially. Keep in Firebase/Supabase if later adding regression. | --- ## Installation & Setup ### Frontend Setup ```bash # Create Flutter project flutter create sage --platforms android,web cd sage # Add dependencies flutter pub add \ flutter_riverpod:^2.6.0 \ riverpod_generator:^2.6.0 \ build_runner:^2.4.0 \ supabase_flutter:^2.0.0 \ mobile_scanner:^7.1.4 \ hive:^2.2.3 \ hive_flutter:^1.1.0 \ go_router:^14.0.0 # Dev dependencies flutter pub add -d \ riverpod_generator:^2.6.0 \ build_runner:^2.4.0 # Generate Riverpod code dart run build_runner watch -d # Run flutter run -d android flutter run -d chrome # Web ``` ### Backend Setup (Supabase Cloud Free Tier) 1. Sign up: https://supabase.com 2. Create project (auto-pauses after 7 days inactivity) 3. Go to SQL Editor → paste schema (see Database Schema above) 4. Enable Row Level Security on all tables 5. In Flutter app: ```dart // Initialize Supabase void main() async { WidgetsFlutterBinding.ensureInitialized(); await Supabase.initialize( url: 'https://your-project.supabase.co', anonKey: 'your-anon-key', // Safe to commit ); runApp(const MyApp()); } ``` ### Backend Setup (Self-Hosted Docker) ```bash # Clone Supabase git clone https://github.com/supabase/supabase.git cd supabase/docker # Configure cp .env.example .env # Edit .env: Set secure JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY # Example: Generate keys with: openssl rand -base64 32 # Start docker-compose up -d # Access: # Supabase Studio: http://localhost:3000 # API: http://localhost:8000 # Realtime: ws://localhost:8000 ``` --- ## Alternatives Considered | Category | Recommended | Alternative | Why Not Recommended | |----------|-------------|-------------|-------------------| | **Backend** | Supabase PostgreSQL | Firebase Firestore | Per-document billing unpredictable with realtime listeners. Firestore doesn't support complex joins needed for household + inventory queries. No self-hosting option. | | **Backend** | Supabase PostgreSQL | Appwrite | Appwrite self-hosted is free, but requires more infrastructure overhead (Docker Swarm/K8s). Supabase's free tier is more mature. Multi-language function support in Appwrite less relevant for this project. | | **State Mgmt** | Riverpod 3.0 | BLoC | BLoC more boilerplate, less suitable for shared web/mobile. Riverpod's mutations (new in 3.0) simplify writes. | | **State Mgmt** | Riverpod 3.0 | Provider | Provider lacks compile-time safety and automatic retry. Riverpod newer, actively developed. | | **Realtime Sync** | Supabase Realtime | Firebase Realtime DB | Firebase charges per read/listener. Less suitable for multi-tenant. Supabase cheaper at scale. | | **Local Cache** | Hive | SQLite | SQLite slower for simple K-V. Hive encrypted by default. | | **Barcode Scanning** | mobile_scanner | flutter_zxing | mobile_scanner uses native APIs (ML Kit/Vision Kit) = more accurate. ZXing pure Dart = slower on mobile. | | **Barcode Data** | Open Food Facts | Barcode Lookup API | OFF more comprehensive nutritional data. BarcodeAPI requires paid tiers for volume. | --- ## Free Tier Gotchas & Mitigation ### Supabase Hosted Free Tier **Gotcha 1: Inactivity Pause** - Projects pause after 7 days without API calls - Even a static homepage doesn't count (must hit database) - **Mitigation:** - Add GitHub Actions to ping API weekly: `curl https://project.supabase.co/rest/v1/tables` - OR use self-hosted Docker (eliminates pausing) **Gotcha 2: Storage Limit (500 MB)** - Sufficient for 10K food items with product metadata (JSON) - NOT sufficient if storing food photos - **Mitigation:** Defer photos to post-MVP or use S3 integration (paid) **Gotcha 3: Realtime Concurrent Connections (200 peak)** - Small households fine, but if scaling to 200+ active households simultaneously = limit reached - Each browser tab = one connection - **Mitigation:** Implement connection pooling or move to Pro ($25/month) for 500 connections **Gotcha 4: Database Egress (5 GB/month)** - Each byte downloaded from API counts - Heavy sync + large payloads = rapid consumption - **Mitigation:** Implement pagination, compress JSON, use partial selects (`select: 'id,name,expiry_date'`) ### Firebase (Not Recommended, Included for Comparison) **Gotcha 1: Per-Read Billing** - Listening to query = charged per document in result set - Small household = 50 items, listener active = 50 reads per update - At scale: $50+/month easily **Gotcha 2: One Free Database Per Project** - Named databases not included in free tier - Complex queries across databases cost double --- ## Recommended Local Development Workflow ### Development ```bash # Terminal 1: Supabase Docker (if developing offline) cd supabase/docker && docker-compose up # Terminal 2: Flutter flutter run -d chrome # Or android emulator # Terminal 3: Run code gen dart run build_runner watch -d ``` ### Environment Variables Create `lib/config/secrets.dart` (add to `.gitignore`): ```dart const String SUPABASE_URL = 'https://your-project.supabase.co'; const String SUPABASE_ANON_KEY = 'your-key'; const String OPEN_FOOD_FACTS_USER_AGENT = 'Sage/1.0'; ``` Or use `.env` + `flutter_dotenv` package for easier secrets management. --- ## Version Pinning Strategy ```yaml # pubspec.yaml dependencies: flutter_riverpod: ^3.2.0 # Use 3.x for mutations supabase_flutter: ^2.0.0 # Stable 2.x branch mobile_scanner: ^7.1.4 # Latest 7.x hive: ^2.2.3 go_router: ^14.0.0 # Lock critical versions riverpod_generator: 2.6.0 # Exact for code generation consistency build_runner: 2.4.0 dev_dependencies: flutter_lints: latest ``` Lock `riverpod_generator` to ensure consistent code generation across team. --- ## Sources ### State Management - [Flutter State Management Options (Official Docs)](https://docs.flutter.dev/data-and-backend/state-mgmt/options) - [State Management in Flutter: 7 Approaches to Know (2026)](https://www.f22labs.com/blogs/state-management-in-flutter-7-approaches-to-know-2025/) - [Ultimate Guide to Flutter State Management 2026: Riverpod](https://medium.com/@satishparmarparmar486/the-ultimate-guide-to-flutter-state-management-in-2026-from-setstate-to-bloc-riverpod-561192c31e1c) ### Supabase & Backend - [Supabase Pricing 2026](https://supabase.com/pricing) - [Supabase Review 2026: Free Tier Limits & Gotchas](https://hackceleration.com/supabase-review/) - [Prevent Supabase Free Tier Pausing (2026 Guide)](https://shadhujan.medium.com/how-to-keep-supabase-free-tier-projects-active-d60fd4a17263) - [Self-Hosting Supabase with Docker](https://supabase.com/docs/guides/self-hosting/docker) - [Designing Postgres Database for Multi-tenancy](https://www.crunchydata.com/blog/designing-your-postgres-database-for-multi-tenancy) ### Firebase Comparison - [Firebase vs Supabase for Flutter (2026)](https://medium.com/@mdazadhossain95/firebase-vs-supabase-for-flutter-apps-which-one-should-you-choose-0cf9dd897123) - [Firestore Quotas and Limits](https://firebase.google.com/docs/firestore/quotas) - [Firebase Free Tier Gotchas](https://supertokens.com/blog/firebase-pricing) ### Barcode & Product Data - [Open Food Facts API Documentation](https://openfoodfacts.github.io/openfoodfacts-server/api/) - [Open Food Facts API Rate Limits](https://openfoodfacts.github.io/documentation/docs/Product-Opener/api/) - [Mobile Scanner 7.1.4 (pub.dev)](https://pub.dev/packages/mobile_scanner) - [Popular Flutter Barcode Scanners](https://scanbot.io/blog/popular-open-source-flutter-barcode-scanners/) - [Barcode Lookup APIs (2026)](https://www.barcodelookup.com/api) ### Local Storage - [Flutter Local Storage Comparison: Hive vs SharedPreferences (2026)](https://medium.com/@taufik.amary/local-storage-comparison-in-flutter-sharedpreferences-hive-isar-and-objectbox-eb9d9ef9a712) ### Notifications - [ntfy.sh: Open Source Push Notifications](https://ntfy.sh/) - [ntfy Integrations & Projects](https://docs.ntfy.sh/integrations/) --- ## Next Steps 1. **Phase 1 (MVP):** Supabase hosted free tier. Add inactivity ping job immediately. 2. **Phase 2+:** Consider self-hosted Docker if inactivity becomes pain point. 3. **Future:** Firebase alternative analysis only if Supabase free tier limits become blocker (unlikely given household scale). 4. **Scaling:** Pro tier ($25/month) if reaching 200+ concurrent households before monetization.