Files
Sage/.planning/research/STACK.md
Dani B bd477b0baa docs: complete project research (ecosystem analysis)
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>
2026-01-27 23:42:37 -05:00

522 lines
20 KiB
Markdown

# 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<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**
```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.