Research files synthesized: - STACK.md: Flutter + Supabase + Riverpod recommended stack - FEATURES.md: 7 table stakes, 6 differentiators, 7 anti-features identified - ARCHITECTURE.md: Offline-first sync with optimistic locking, RLS multi-tenancy - PITFALLS.md: 5 critical pitfalls (v1), 8 moderate (v1.5), 3 minor (v2+) - SUMMARY.md: Executive synthesis with 3-phase roadmap implications Key findings: - Stack: Flutter + Supabase free tier + mobile_scanner + Open Food Facts - Critical pitfalls: Barcode mismatches, timezone bugs, sync conflicts, setup complexity, notification fatigue - Phase structure: MVP (core) → expansion (usage tracking) → differentiation (prediction + sales) - All research grounded in ecosystem analysis (12+ competitors), official documentation, and production incidents Confidence: HIGH Ready for roadmap creation: YES Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
20 KiB
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
FutureProviderwraps Supabase calls (read-only) - Riverpod
Notifier+NotifierProviderfor 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:
# 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)
-- 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):
// Riverpod provider subscribes to food_items table changes
final inventoryProvider = StreamProvider<List<FoodItem>>((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<dynamic> 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
// 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
# 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)
- Sign up: https://supabase.com
- Create project (auto-pauses after 7 days inactivity)
- Go to SQL Editor → paste schema (see Database Schema above)
- Enable Row Level Security on all tables
- In Flutter app:
// 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)
# 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)
- Add GitHub Actions to ping API weekly:
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
# 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):
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
# 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)
- State Management in Flutter: 7 Approaches to Know (2026)
- Ultimate Guide to Flutter State Management 2026: Riverpod
Supabase & Backend
- Supabase Pricing 2026
- Supabase Review 2026: Free Tier Limits & Gotchas
- Prevent Supabase Free Tier Pausing (2026 Guide)
- Self-Hosting Supabase with Docker
- Designing Postgres Database for Multi-tenancy
Firebase Comparison
Barcode & Product Data
- Open Food Facts API Documentation
- Open Food Facts API Rate Limits
- Mobile Scanner 7.1.4 (pub.dev)
- Popular Flutter Barcode Scanners
- Barcode Lookup APIs (2026)
Local Storage
Notifications
Next Steps
- Phase 1 (MVP): Supabase hosted free tier. Add inactivity ping job immediately.
- Phase 2+: Consider self-hosted Docker if inactivity becomes pain point.
- Future: Firebase alternative analysis only if Supabase free tier limits become blocker (unlikely given household scale).
- Scaling: Pro tier ($25/month) if reaching 200+ concurrent households before monetization.