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

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.


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:

# 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)

  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:
// 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)

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')

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

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

Supabase & Backend

Firebase Comparison

Barcode & Product Data

Local Storage

Notifications


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.