✨ Features implemented: - Smart inventory tracking with Hive database - Barcode scanning with auto-populated product info - Multiple API fallbacks (Open Food Facts, UPCItemDB) - Smart expiration date predictions by category - Discord webhook notifications (persisted) - Custom sage leaf vector icon - Material Design 3 UI with sage green theme - Privacy Policy & Terms of Service - Local-first, privacy-focused architecture 🎨 UI/UX: - Home dashboard with inventory stats - Add Item screen with barcode integration - Inventory list with expiration indicators - Settings with persistent preferences - About section with legal docs 🔧 Technical: - Flutter 3.35.5 with Riverpod state management - Hive 2.2.3 for local database - Mobile scanner for barcode detection - Feature-first architecture 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
42 KiB
SAGE - COMPLETE PROJECT DOCUMENTATION
Smart Kitchen Management System 🌿
Version: 1.0.0
Created: October 3, 2025
Platform: Flutter (Android & iOS)
Status: Active Development 🔥
📖 TABLE OF CONTENTS
- Project Overview
- Core Features
- Technical Architecture
- Data Models
- API Integrations
- User Flows
- Notification System
- Multi-User & Sync
- Security & Privacy
- Testing Strategy
- Deployment
📱 PROJECT OVERVIEW
The Problem
- People forget what's in their fridge/pantry
- Food expires and gets wasted → money wasted
- Constant grocery store trips forgetting items
- End up ordering takeout when food at home expires
- No organization = kitchen chaos
The Solution: SAGE
A comprehensive kitchen management app that:
- Tracks all your food inventory with expiration dates
- Alerts you before food expires (with Discord integration!)
- Manages your recipes and connects them to inventory
- Generates smart shopping lists
- Shares with household members
- Works offline-first with optional cloud sync
Why "Sage"?
- 🌿 The herb (kitchen theme)
- 🧠 Wisdom and smart decisions
- 💚 Simple, clean, memorable
Target Users
- Busy individuals who want to save money
- Families managing shared kitchens
- Anyone tired of wasting food
- People who hate grocery shopping inefficiency
- Budget-conscious cooks
🎯 CORE FEATURES
1. INVENTORY TRACKER 📦
The Foundation: Track everything in your kitchen
Key Capabilities:
- Add items via barcode scan OR manual entry
- Auto-populate from Open Food Facts (2M+ products!)
- Track quantity, expiration date, location
- Organize by location (fridge, freezer, pantry, etc.)
- Categorize automatically or manually
- Attach photos (cached locally)
- Search and filter inventory
- Quick edit/delete items
Barcode Scanning:
User scans barcode
↓
Query Open Food Facts API
↓
Product found?
├─ YES → Auto-fill: name, photo, category, typical shelf life
│ User adds: quantity, actual expiration, location
│ → Save to local database
│
└─ NO → Manual entry form
User fills everything
→ Save to local database
Data Tracked Per Item:
- Name
- Barcode (if scanned)
- Quantity & unit
- Purchase date
- Expiration date
- Location (fridge/freezer/pantry/other)
- Category (auto or manual)
- Photo URL (cached)
- Notes (optional)
- Last modified timestamp
- Sync status
2. SMART ALERTS 🔔
The Brain: Never let food go to waste
Alert Timeline:
Time Before Expiration | Alert Type | Message |
---|---|---|
1 month | FYI notification | "Ranch dressing expires in 30 days" |
2 weeks | Heads up | "Sour cream expires in 14 days" |
1 week | Use soon! | "Milk expires in 7 days - Tap for recipes" |
Before next shopping day | Shopping alert | "3 items expire before next week!" |
Shopping Day Intelligence:
User sets shopping frequency:
- Weekly (every Tuesday)
- Bi-weekly (every other Monday)
- Monthly (first Saturday)
- Pay cycle (15th and 30th)
- Custom days
System calculates next shopping day
↓
For each item expiring before next shopping day:
→ Send notification
→ Add to "Add to Shopping List" quick action
→ Send to Discord channel (if enabled)
Notification Types:
-
Local Push Notifications
- Scheduled in advance
- Tappable (opens relevant screen)
- Customizable (can disable certain types)
-
Discord Webhooks
- Critical alerts only (shopping day, urgent expirations)
- Formatted with emojis and priority
- @everyone tag for household
Example Discord Message:
🌿 **Sage Alert** 🌿
@everyone Shopping day is tomorrow!
**Expiring before next week:**
🔴 Ranch dressing (expires Oct 10) - 3 days
🔴 Croutons (expires Oct 12) - 5 days
🟡 Milk (expires Oct 15) - 8 days
**Suggested Actions:**
• Add to shopping list
• Check "Use It Up" recipes
• Update quantities if already replaced
Don't forget to restock! 🛒
3. RECIPE BOOK 📖
The Inspiration: Never wonder "what's for dinner?" again
Core Functions:
- Add recipes manually or copy/paste from websites
- Store ingredients with quantities
- Write/paste instructions
- Add optional photos
- Tag recipes (Quick, Vegetarian, Fancy, etc.)
- Share recipes with community (optional)
- Mark favorites
Smart Features:
-
"What Can I Make?" - Shows recipes you can make with current inventory
- Displays % of ingredients you have
- Sorts by completeness
- Shows what's missing
-
"Use It Up!" - Suggests recipes using expiring items
- Prioritizes items expiring soonest
- Helps prevent waste
Recipe Data Structure:
Recipe {
name: "Caesar Salad"
ingredients: [
{name: "Romaine lettuce", quantity: 1, unit: "head"},
{name: "Caesar dressing", quantity: 0.5, unit: "cup"},
{name: "Croutons", quantity: 1, unit: "cup"},
{name: "Parmesan", quantity: 0.25, unit: "cup", optional: true}
]
instructions: "Chop lettuce. Toss with dressing. Top with croutons and parmesan."
prepTime: 10 minutes
servings: 2
tags: ["Quick", "Salad", "Vegetarian"]
photo: "url_or_local_path"
isPublic: true // Share with community
createdBy: user_id
}
Copy/Paste from Websites: User pastes URL or text block → Parser extracts:
- Recipe title
- Ingredient list (with quantities)
- Instructions
- Optional: photo, prep time, servings
Common formats supported:
- Recipe schema (schema.org/Recipe)
- Plain text with smart parsing
- Popular recipe sites (AllRecipes, Food Network, etc.)
4. SHOPPING LISTS 🛒
The Planner: Efficient grocery trips, every time
Key Features:
-
Multiple Lists - Separate lists for different stores
- "Costco"
- "Trader Joe's"
- "Farmers Market"
- Custom names
-
Smart List Building
- Add items manually
- One-click add from recipes
- Suggested items based on expiring inventory
-
While Shopping
- Check off items as you shop
- Uncheck if needed
- Sort by store section (optional)
- Quick quantity adjust
-
After Shopping
- "Add All to Inventory" quick action
- Batch-add checked items
- Auto-populate likely expiration dates
- Clear completed items
Sharing:
- Share lists with household members
- Real-time updates (if cloud sync enabled)
- See who added what
- Collaborative shopping
List Organization:
Shopping List: "Costco"
Items:
☐ Milk (2 gallons) - Added from: Ranch expires alert
☑ Chicken breast (2 lbs) - Added from: Recipe "Chicken Tacos"
☐ Romaine lettuce (2 heads) - Added manually
☐ Croutons (1 bag) - Added from: Recipe "Caesar Salad"
Quick Actions:
- Add recipe ingredients
- Add expiring items
- Share list
- Sort by aisle
5. MULTI-USER & SYNC ☁️
The Connector: Share with your household
Three Sync Modes (User's Choice):
Mode 1: Cloud Sync (Supabase) ☁️
Best for: Most users, easy setup, reliable
Features:
- Real-time sync across devices
- User authentication
- Household groups (shared inventory/lists)
- Recipe community sharing
- Automatic backups
Technical:
- PostgreSQL database (Supabase)
- Row-level security
- Real-time subscriptions
- Free tier: 500MB DB, 2GB bandwidth/month
User Flow:
Sign up with email
↓
Create or join household
↓
All devices sync automatically
↓
Changes propagate in real-time
Mode 2: Completely Free (Self-Hosted) 🏠
Best for: Privacy-conscious users, no cloud dependency
Options:
A. Export/Import
- Export inventory/recipes to JSON
- Share file via any method (email, cloud storage, etc.)
- Import on other device
- Manual sync process
B. Local WiFi Sync
- Devices on same network
- Peer-to-peer sync (no internet needed!)
- Automatic discovery
- One device = "host"
C. Self-Host Backend
- Docker Compose setup (we provide!)
- Run on Raspberry Pi or home server
- Full control of data
- Zero recurring costs
Self-Hosted Setup:
# We provide this in docs!
git clone https://github.com/sage-app/backend
cd backend
docker-compose up -d
# Configure app to point to your server
# http://192.168.1.100:3000
Mode 3: Local Only 📱
Best for: Solo users, maximum privacy, no sharing needed
Features:
- Everything on device
- Zero external dependencies
- Export for backup
- Fast and simple
Offline-First Philosophy (ALL MODES):
Every action saves locally FIRST
↓
If online & sync enabled:
Queue for sync
↓
Sync when possible
↓
Update local cache
App works PERFECTLY offline
Never wait for network
Graceful sync when available
Conflict Resolution:
- Last-write-wins (timestamp-based)
- Show notification if conflict detected
- User can view both versions and choose
🏗️ TECHNICAL ARCHITECTURE
Tech Stack Overview
┌─────────────────────────────────────┐
│ FLUTTER APP (Dart) │
│ ┌───────────────────────────────┐ │
│ │ UI Layer (Widgets) │ │
│ └──────────┬────────────────────┘ │
│ │ │
│ ┌──────────▼────────────────────┐ │
│ │ State Management (Riverpod) │ │
│ └──────────┬────────────────────┘ │
│ │ │
│ ┌──────────▼────────────────────┐ │
│ │ Business Logic Layer │ │
│ │ - Controllers │ │
│ │ - Services │ │
│ │ - Repositories │ │
│ └──────────┬────────────────────┘ │
│ │ │
│ ┌──────────▼────────────────────┐ │
│ │ Data Layer │ │
│ │ ┌──────────┐ ┌────────────┐ │ │
│ │ │ Isar │ │ Supabase │ │ │
│ │ │ (Local) │ │ (Cloud) │ │ │
│ │ └──────────┘ └────────────┘ │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
│
▼
┌────────────────────┐
│ External APIs │
│ - Open Food Facts │
│ - Discord Webhook │
└────────────────────┘
Key Technologies
Component | Technology | Purpose |
---|---|---|
Framework | Flutter 3.x | Cross-platform UI |
Language | Dart 3.x | Type-safe, fast, AOT compiled |
State Management | Riverpod 2.x | Reactive state, dependency injection |
Local Database | Isar 3.x | Fast NoSQL, offline-first |
Cloud Database | Supabase | PostgreSQL, real-time, auth |
Barcode Scanner | mobile_scanner | Camera-based scanning |
Notifications | flutter_local_notifications | Scheduled alerts |
Image Caching | cached_network_image | Performance optimization |
HTTP Client | http | API requests |
Project Structure
sage/
├── lib/
│ ├── main.dart # App entry point
│ ├── app.dart # App widget, theme, routing
│ │
│ ├── core/ # Core utilities
│ │ ├── constants/
│ │ │ ├── app_constants.dart
│ │ │ ├── api_constants.dart
│ │ │ └── colors.dart
│ │ ├── utils/
│ │ │ ├── date_utils.dart
│ │ │ ├── validators.dart
│ │ │ └── formatters.dart
│ │ └── extensions/
│ │ ├── date_extensions.dart
│ │ └── string_extensions.dart
│ │
│ ├── features/ # Feature modules
│ │ ├── inventory/
│ │ │ ├── models/
│ │ │ │ └── food_item.dart
│ │ │ ├── repositories/
│ │ │ │ └── inventory_repository.dart
│ │ │ ├── controllers/
│ │ │ │ └── inventory_controller.dart
│ │ │ ├── screens/
│ │ │ │ ├── inventory_screen.dart
│ │ │ │ ├── add_item_screen.dart
│ │ │ │ └── item_detail_screen.dart
│ │ │ └── widgets/
│ │ │ ├── inventory_list.dart
│ │ │ ├── expiration_badge.dart
│ │ │ └── barcode_scanner.dart
│ │ │
│ │ ├── recipes/
│ │ │ ├── models/
│ │ │ │ └── recipe.dart
│ │ │ ├── repositories/
│ │ │ │ └── recipe_repository.dart
│ │ │ ├── controllers/
│ │ │ │ └── recipe_controller.dart
│ │ │ ├── screens/
│ │ │ │ ├── recipes_screen.dart
│ │ │ │ ├── add_recipe_screen.dart
│ │ │ │ ├── recipe_detail_screen.dart
│ │ │ │ └── what_can_i_make_screen.dart
│ │ │ └── widgets/
│ │ │ ├── recipe_card.dart
│ │ │ └── ingredient_list.dart
│ │ │
│ │ ├── shopping/
│ │ │ ├── models/
│ │ │ │ └── shopping_list.dart
│ │ │ ├── repositories/
│ │ │ │ └── shopping_repository.dart
│ │ │ ├── controllers/
│ │ │ │ └── shopping_controller.dart
│ │ │ ├── screens/
│ │ │ │ ├── shopping_lists_screen.dart
│ │ │ │ └── list_detail_screen.dart
│ │ │ └── widgets/
│ │ │ └── shopping_item.dart
│ │ │
│ │ ├── home/
│ │ │ ├── screens/
│ │ │ │ └── home_screen.dart
│ │ │ └── widgets/
│ │ │ ├── expiring_soon_carousel.dart
│ │ │ └── quick_stats.dart
│ │ │
│ │ ├── settings/
│ │ │ ├── models/
│ │ │ │ └── user_settings.dart
│ │ │ ├── repositories/
│ │ │ │ └── settings_repository.dart
│ │ │ ├── controllers/
│ │ │ │ └── settings_controller.dart
│ │ │ └── screens/
│ │ │ ├── settings_screen.dart
│ │ │ ├── notifications_settings.dart
│ │ │ ├── sync_settings.dart
│ │ │ └── discord_settings.dart
│ │ │
│ │ └── auth/
│ │ ├── controllers/
│ │ │ └── auth_controller.dart
│ │ └── screens/
│ │ ├── login_screen.dart
│ │ └── signup_screen.dart
│ │
│ ├── services/ # Business logic services
│ │ ├── notification_service.dart
│ │ ├── sync_service.dart
│ │ ├── barcode_service.dart
│ │ ├── discord_service.dart
│ │ ├── openfoodfacts_service.dart
│ │ └── expiration_tracker_service.dart
│ │
│ ├── data/ # Data layer
│ │ ├── local/
│ │ │ └── isar_database.dart
│ │ └── remote/
│ │ └── supabase_client.dart
│ │
│ └── shared/ # Shared widgets & components
│ ├── widgets/
│ │ ├── custom_button.dart
│ │ ├── custom_text_field.dart
│ │ └── loading_indicator.dart
│ └── navigation/
│ └── app_router.dart
│
├── assets/ # Static assets
│ ├── images/
│ ├── icons/
│ └── fonts/
│
├── test/ # Tests
│ ├── unit/
│ ├── widget/
│ └── integration/
│
├── android/ # Android-specific
├── ios/ # iOS-specific
├── pubspec.yaml # Dependencies
└── README.md
💾 DATA MODELS (DETAILED)
FoodItem Model
import 'package:isar/isar.dart';
part 'food_item.g.dart';
@collection
class FoodItem {
Id id = Isar.autoIncrement;
// Basic Info
late String name;
String? barcode;
late int quantity;
String? unit; // "bottles", "lbs", "oz", "items"
// Dates
late DateTime purchaseDate;
late DateTime expirationDate;
// Organization
@enumerated
late Location location;
String? category; // Auto from barcode or manual
// Media & Notes
String? photoUrl; // Cached from API or user uploaded
String? notes;
// Multi-user support
String? userId;
String? householdId;
// Sync tracking
DateTime? lastModified;
bool syncedToCloud = false;
// Computed properties (not stored)
@ignore
int get daysUntilExpiration {
return expirationDate.difference(DateTime.now()).inDays;
}
@ignore
ExpirationStatus get expirationStatus {
final days = daysUntilExpiration;
if (days < 0) return ExpirationStatus.expired;
if (days <= 3) return ExpirationStatus.critical;
if (days <= 7) return ExpirationStatus.warning;
if (days <= 14) return ExpirationStatus.caution;
return ExpirationStatus.fresh;
}
}
enum Location {
fridge,
freezer,
pantry,
spiceRack,
countertop,
other
}
enum ExpirationStatus {
fresh, // > 14 days
caution, // 8-14 days
warning, // 4-7 days
critical, // 1-3 days
expired // 0 or negative days
}
Recipe Model
import 'package:isar/isar.dart';
part 'recipe.g.dart';
@collection
class Recipe {
Id id = Isar.autoIncrement;
// Basic Info
late String name;
late String instructions;
// Ingredients (embedded objects)
late List<Ingredient> ingredients;
// Media
String? photoUrl;
// Metadata
List<String> tags = [];
int? prepTimeMinutes;
int? cookTimeMinutes;
int? servings;
@enumerated
DifficultyLevel? difficulty;
// Sharing
String? createdBy; // user_id
bool isPublic = false;
bool isFavorite = false;
// Timestamps
DateTime? created;
DateTime? lastModified;
// Sync
bool syncedToCloud = false;
// Computed
@ignore
int get totalTimeMinutes {
return (prepTimeMinutes ?? 0) + (cookTimeMinutes ?? 0);
}
}
@embedded
class Ingredient {
late String name;
double? quantity;
String? unit;
bool optional = false;
// For matching against inventory
String? matchedItemId; // Link to FoodItem
}
enum DifficultyLevel {
easy,
medium,
hard
}
ShoppingList Model
import 'package:isar/isar.dart';
part 'shopping_list.g.dart';
@collection
class ShoppingList {
Id id = Isar.autoIncrement;
late String name; // "Costco", "Trader Joe's"
List<ShoppingItem> items = [];
// Sharing
String? householdId;
List<String> sharedWith = [];
// Metadata
DateTime? created;
DateTime? lastModified;
// Sync
bool syncedToCloud = false;
// Computed
@ignore
int get totalItems => items.length;
@ignore
int get checkedItems => items.where((i) => i.checked).length;
@ignore
double get completionPercentage {
if (totalItems == 0) return 0;
return (checkedItems / totalItems) * 100;
}
}
@embedded
class ShoppingItem {
late String name;
double? quantity;
String? unit;
bool checked = false;
// Metadata
@enumerated
Priority priority = Priority.normal;
String? addedFrom; // "recipe:123", "inventory:456", "manual"
String? addedBy; // user_id
DateTime? addedAt;
String? notes;
}
enum Priority {
low,
normal,
high
}
UserSettings Model
import 'package:isar/isar.dart';
part 'user_settings.g.dart';
@collection
class UserSettings {
Id id = Isar.autoIncrement;
// Shopping Schedule
@enumerated
ShoppingFrequency frequency = ShoppingFrequency.weekly;
// For weekly/biweekly
int? dayOfWeek; // 1-7 (Monday-Sunday)
// For monthly
int? dayOfMonth; // 1-31
// For pay cycle
List<int>? payCycleDays; // [15, 30]
// For custom
List<int>? customDays; // [1, 4, 6] = Mon, Thu, Sat
// Calculated
DateTime? nextShoppingDay;
// Notifications
bool enableNotifications = true;
bool notify1Month = true;
bool notify2Weeks = true;
bool notify1Week = true;
bool notifyShoppingDay = true;
// Discord
bool enableDiscord = false;
String? discordWebhookUrl;
bool discordCriticalOnly = true;
// Sync
@enumerated
SyncMode syncMode = SyncMode.localOnly;
String? supabaseUserId;
String? householdId;
// Display
bool showPhotos = true;
bool showExpirationBadges = true;
@enumerated
ThemeMode themeMode = ThemeMode.system;
@enumerated
SortOrder inventorySortOrder = SortOrder.expirationDate;
}
enum ShoppingFrequency {
weekly, // Every X day of week
biweekly, // Every 2 weeks on X day
monthly, // Every month on X date
payCycle, // Specific dates (15th, 30th)
custom // User-selected days
}
enum SyncMode {
localOnly,
cloudSync,
wifiSync,
selfHosted
}
enum ThemeMode {
light,
dark,
system
}
enum SortOrder {
name,
expirationDate,
purchaseDate,
location,
quantity
}
🔌 API INTEGRATIONS
1. Open Food Facts API
Purpose: Auto-populate product info from barcodes
Base URL: https://world.openfoodfacts.org/api/v2
Endpoint: GET /product/{barcode}
Example Request:
final response = await http.get(
Uri.parse('https://world.openfoodfacts.org/api/v2/product/0041220576500')
);
// Returns product data including:
// - product_name
// - brands
// - categories
// - image_url
// - ingredients
// - nutriscore
Example Response:
{
"code": "0041220576500",
"product": {
"product_name": "Hidden Valley Ranch Dressing",
"brands": "Hidden Valley",
"categories": "Dressings, Ranch dressing",
"image_url": "https://images.openfoodfacts.org/...",
"quantity": "16 fl oz (473 mL)"
},
"status": 1,
"status_verbose": "product found"
}
Service Implementation:
class OpenFoodFactsService {
static const baseUrl = 'https://world.openfoodfacts.org/api/v2';
Future<ProductInfo?> getProductByBarcode(String barcode) async {
try {
final response = await http.get(
Uri.parse('$baseUrl/product/$barcode')
);
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['status'] == 1) {
return ProductInfo.fromJson(data['product']);
}
}
return null; // Product not found
} catch (e) {
print('Error fetching product: $e');
return null;
}
}
}
Fallback Strategy:
- Try Open Food Facts
- If not found, try UPC Database API (backup)
- If still not found, manual entry
2. Discord Webhooks
Purpose: Send expiration alerts to Discord channel
Setup:
- User creates webhook in Discord server settings
- Copies webhook URL
- Pastes in Sage settings
- App sends JSON POST requests to webhook
Example Webhook URL:
https://discord.com/api/webhooks/123456789/abcdef...
Sending a Message:
class DiscordService {
Future<void> sendAlert(String webhookUrl, List<FoodItem> expiringItems) async {
final message = _buildAlertMessage(expiringItems);
final response = await http.post(
Uri.parse(webhookUrl),
headers: {'Content-Type': 'application/json'},
body: json.encode({
'username': 'Sage Kitchen Alert',
'avatar_url': 'https://sage-app.com/icon.png',
'embeds': [
{
'title': '🌿 Sage Alert 🌿',
'description': message,
'color': 0x4CAF50, // Green
'footer': {
'text': 'Sage Kitchen Management'
},
'timestamp': DateTime.now().toIso8601String()
}
]
})
);
if (response.statusCode != 204) {
throw Exception('Failed to send Discord alert');
}
}
String _buildAlertMessage(List<FoodItem> items) {
final buffer = StringBuffer();
buffer.writeln('@everyone Shopping day is tomorrow!\n');
buffer.writeln('**Expiring before next week:**');
for (final item in items) {
final emoji = _getEmoji(item.daysUntilExpiration);
buffer.writeln('$emoji ${item.name} (expires in ${item.daysUntilExpiration} days)');
}
buffer.writeln('\nDon\'t forget to restock! 🛒');
return buffer.toString();
}
String _getEmoji(int days) {
if (days <= 3) return '🔴';
if (days <= 7) return '🟡';
return '🟢';
}
}
Rate Limiting:
- Discord allows ~5 requests per second per webhook
- Batch alerts together
- Use exponential backoff if rate limited
📱 USER FLOWS
Flow 1: First Time Setup
User downloads Sage
↓
Welcome screen
↓
Choose sync mode:
- Local only (skip to next)
- Cloud sync (sign up/login)
- Self-hosted (enter URL)
↓
Set shopping frequency:
"How often do you shop?"
- Weekly → Pick day
- Bi-weekly → Pick day
- Monthly → Pick date
- Pay cycle → Enter dates
- Custom → Select days
↓
Enable notifications:
"Get alerts for expiring items?"
- Yes (request permission)
- No (can enable later)
↓
Optional: Discord setup
"Send alerts to Discord?"
- Yes → Paste webhook URL → Test
- Skip
↓
Tutorial:
"Let's add your first item!"
- Scan barcode demo
- OR manual entry
↓
Ready to use! 🎉
Flow 2: Adding Item via Barcode
User taps "+ Add Item"
↓
Choose method:
[📷 Scan Barcode] or [✏️ Manual Entry]
↓
User taps "Scan Barcode"
↓
Camera opens with barcode overlay
↓
User scans barcode
↓
Query Open Food Facts API
↓
┌─────────────────────┐
│ Product Found? │
└────┬────────────┬───┘
│ │
YES NO
│ │
▼ ▼
Auto-fill: Manual Entry:
- Name - Type name
- Photo - Optional photo
- Category - Select category
│ │
└────┬───────┘
▼
User adds:
- Quantity (1, 2, 3...)
- Unit (bottles, lbs, etc.)
- Expiration date (calendar picker)
- Location (dropdown: Fridge/Freezer/Pantry)
- Optional notes
↓
[Save] button
↓
Save to local database
↓
If cloud sync enabled:
Queue for sync
↓
Show success message
Navigate to inventory screen
Flow 3: Meal Planning with Recipes
User opens Recipes tab
↓
Browses recipes OR taps "What Can I Make?"
↓
IF "What Can I Make?":
System matches recipes to current inventory
Shows % of ingredients available
"Caesar Salad: 75% (missing croutons)"
↓
User selects recipe
↓
Recipe details screen shows:
- Photo
- Ingredients with checkmarks:
✅ Romaine lettuce (in fridge)
✅ Ranch dressing (in pantry)
❌ Croutons (not in inventory)
- Instructions
- Prep time
↓
User taps "Add Missing Items to Shopping List"
↓
Dialog: "Add to which list?"
- Select existing list
- OR create new list
↓
Croutons added to shopping list
↓
User cooks meal!
↓
Optional: Mark ingredients as used
(Reduces quantity in inventory)
Flow 4: Shopping Trip
User opens Shopping tab
↓
Selects "Costco" shopping list
↓
List shows:
☐ Milk (2 gallons)
☐ Chicken (2 lbs)
☐ Croutons (1 bag)
☑ Eggs (already checked off yesterday)
↓
At store, user checks off items:
☑ Milk
☑ Chicken
☑ Croutons
↓
All items checked!
↓
Banner appears:
"Shopping complete! Add items to inventory?"
[Yes] [Not yet]
↓
User taps [Yes]
↓
Bulk add screen:
Pre-filled with checked items
User adds:
- Expiration dates (smart defaults shown)
- Locations (remembered from last time)
Quick swipe interface
↓
[Save All] button
↓
All items added to inventory
Shopping list cleared (or moved to archive)
↓
Success! 🎉
Flow 5: Responding to Expiration Alert
Morning notification:
"🌿 Sage: Use soon!
Ranch dressing expires in 3 days"
↓
User taps notification
↓
App opens to "Use It Up" screen
↓
Shows recipes featuring ranch:
- Buffalo Chicken Wrap
- Veggie Dip Platter
- Ranch Chicken Tacos
↓
User selects "Buffalo Chicken Wrap"
↓
Checks ingredients:
✅ Ranch dressing (expiring!)
✅ Tortillas
✅ Lettuce
❌ Chicken (needs to buy)
↓
Adds chicken to shopping list
↓
Makes dinner, ranch dressing used!
↓
User marks ranch as "used up" in app
OR reduces quantity to 0
↓
Item removed from inventory
Food waste prevented! 💪
🔔 NOTIFICATION SYSTEM (DETAILED)
Background Job Architecture
Strategy: Scheduled daily check + immediate calculations
class ExpirationTrackerService {
final InventoryRepository _inventory;
final SettingsRepository _settings;
final NotificationService _notifications;
Future<void> runDailyCheck() async {
// Run every morning at 9 AM
final items = await _inventory.getAllItems();
final settings = await _settings.getSettings();
final nextShoppingDay = settings.nextShoppingDay;
for (final item in items) {
await _checkItemAndNotify(item, nextShoppingDay);
}
}
Future<void> _checkItemAndNotify(FoodItem item, DateTime? nextShopping) async {
final daysUntil = item.daysUntilExpiration;
// 1 month notification
if (daysUntil == 30) {
await _notifications.schedule(
title: '🌿 Sage: FYI',
body: '${item.name} expires in 30 days',
payload: 'item:${item.id}',
);
}
// 2 weeks notification
if (daysUntil == 14) {
await _notifications.schedule(
title: '🌿 Sage: Heads up!',
body: '${item.name} expires in 2 weeks',
payload: 'item:${item.id}',
);
}
// 1 week notification
if (daysUntil == 7) {
await _notifications.schedule(
title: '🌿 Sage: Use soon!',
body: '${item.name} expires in 1 week - Tap for recipes',
payload: 'useItUp:${item.id}',
);
}
// Shopping day check
if (nextShopping != null) {
if (item.expirationDate.isBefore(nextShopping) && daysUntil > 0) {
await _notifications.schedule(
title: '🌿 Sage: Add to shopping list!',
body: '${item.name} expires before your next shopping trip',
payload: 'addToList:${item.id}',
);
}
}
}
}
Shopping Day Calculation
class ShoppingDayCalculator {
DateTime? calculateNextShoppingDay(UserSettings settings) {
final now = DateTime.now();
switch (settings.frequency) {
case ShoppingFrequency.weekly:
return _nextWeekday(now, settings.dayOfWeek!);
case ShoppingFrequency.biweekly:
return _nextBiweekly(now, settings.dayOfWeek!);
case ShoppingFrequency.monthly:
return _nextMonthlyDate(now, settings.dayOfMonth!);
case ShoppingFrequency.payCycle:
return _nextPayCycleDate(now, settings.payCycleDays!);
case ShoppingFrequency.custom:
return _nextCustomDay(now, settings.customDays!);
}
}
DateTime _nextWeekday(DateTime from, int targetDay) {
// targetDay: 1 = Monday, 7 = Sunday
final currentDay = from.weekday;
int daysToAdd = (targetDay - currentDay) % 7;
if (daysToAdd == 0) daysToAdd = 7; // Next week
return from.add(Duration(days: daysToAdd));
}
DateTime _nextBiweekly(DateTime from, int targetDay) {
final nextWeek = _nextWeekday(from, targetDay);
// Check if last shopping was less than a week ago
// If yes, return 2 weeks from last shopping
// If no, return next week
// (Would need to store last shopping date)
return nextWeek.add(Duration(days: 7)); // Simplified
}
DateTime _nextMonthlyDate(DateTime from, int targetDate) {
var next = DateTime(from.year, from.month, targetDate);
if (next.isBefore(from) || next.isAtSameMomentAs(from)) {
// Next month
next = DateTime(from.year, from.month + 1, targetDate);
}
return next;
}
DateTime _nextPayCycleDate(DateTime from, List<int> dates) {
// dates like [15, 30]
final upcoming = dates
.map((d) => DateTime(from.year, from.month, d))
.where((date) => date.isAfter(from))
.toList()
..sort();
if (upcoming.isEmpty) {
// Next month
final firstDate = dates.first;
return DateTime(from.year, from.month + 1, firstDate);
}
return upcoming.first;
}
DateTime _nextCustomDay(DateTime from, List<int> days) {
// days like [1, 4, 6] = Monday, Thursday, Saturday
final upcoming = days
.map((d) => _nextWeekday(from, d))
.toList()
..sort();
return upcoming.first;
}
}
🔐 SECURITY & PRIVACY
Data Privacy Principles
- Local-First: All data stored locally first, cloud is optional
- User Control: Users choose sync mode and what to share
- No Tracking: Zero analytics, no user tracking
- Open Source: Code is public, verifiable
- Self-Hostable: Users can run own backend
Cloud Security (Supabase)
Row-Level Security (RLS) Policies:
-- Users can only see their own items
CREATE POLICY "Users see own items"
ON food_items
FOR SELECT
USING (auth.uid() = user_id);
-- Household members see shared items
CREATE POLICY "Household access"
ON food_items
FOR SELECT
USING (
household_id IN (
SELECT household_id
FROM household_members
WHERE user_id = auth.uid()
)
);
-- Users can only modify their own items
CREATE POLICY "Users modify own items"
ON food_items
FOR UPDATE
USING (auth.uid() = user_id);
-- Similar policies for recipes, shopping lists, etc.
Authentication:
- Email/password (Supabase Auth)
- Optional: OAuth (Google, Apple) in future
- JWT tokens for API requests
- Refresh tokens for sessions
Local Security
Database Encryption:
// Isar supports encryption
final isar = await Isar.open(
[FoodItemSchema, RecipeSchema, ...],
directory: dir.path,
inspector: false,
encryptionKey: _getOrCreateEncryptionKey(),
);
Secure Storage:
- User settings encrypted
- Discord webhook URL secured
- Auth tokens in secure storage (flutter_secure_storage)
🧪 TESTING STRATEGY
Unit Tests
Test individual functions and business logic
// Example: Test expiration calculation
test('daysUntilExpiration calculates correctly', () {
final item = FoodItem()
..name = 'Milk'
..expirationDate = DateTime.now().add(Duration(days: 5));
expect(item.daysUntilExpiration, equals(5));
});
test('expirationStatus returns correct status', () {
final item = FoodItem()
..name = 'Milk'
..expirationDate = DateTime.now().add(Duration(days: 2));
expect(item.expirationStatus, equals(ExpirationStatus.critical));
});
Widget Tests
Test UI components
testWidgets('ExpirationBadge shows correct color', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: ExpirationBadge(daysUntil: 2),
),
);
expect(find.text('2 days'), findsOneWidget);
final badge = tester.widget<Container>(find.byType(Container));
expect(badge.color, equals(Colors.red)); // Critical = red
});
Integration Tests
Test complete user flows
testWidgets('Add item flow works end-to-end', (tester) async {
await tester.pumpWidget(MyApp());
// Tap add button
await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();
// Choose manual entry
await tester.tap(find.text('Manual Entry'));
await tester.pumpAndSettle();
// Fill form
await tester.enterText(find.byKey(Key('name_field')), 'Milk');
await tester.enterText(find.byKey(Key('quantity_field')), '1');
// Select expiration date
await tester.tap(find.byKey(Key('expiration_picker')));
// ... date picker interaction
// Save
await tester.tap(find.text('Save'));
await tester.pumpAndSettle();
// Verify item appears in inventory
expect(find.text('Milk'), findsOneWidget);
});
🚀 DEPLOYMENT
Android Release Process
- Build Release APK:
flutter build apk --release
- Build App Bundle (for Play Store):
flutter build appbundle --release
-
Sign the Build:
- Configure signing in
android/app/build.gradle
- Store keystore securely
- Add to
.gitignore
- Configure signing in
-
Upload to Play Store:
- Internal testing → Alpha → Beta → Production
- Gradual rollout recommended
iOS Release Process
-
Prerequisites:
- Apple Developer account ($99/year)
- Xcode installed
- Provisioning profiles configured
-
Build:
flutter build ios --release
-
Archive in Xcode:
- Open
ios/Runner.xcworkspace
- Product → Archive
- Upload to App Store Connect
- Open
-
TestFlight:
- Internal testing
- External beta testing
- Submit for review
Self-Hosted Backend Deployment
Docker Compose Setup:
version: '3.8'
services:
postgres:
image: postgres:15
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: sage
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
api:
image: sage-backend:latest
depends_on:
- postgres
environment:
DATABASE_URL: postgresql://postgres:${DB_PASSWORD}@postgres:5432/sage
JWT_SECRET: ${JWT_SECRET}
ports:
- "3000:3000"
volumes:
postgres_data:
Deploy on Raspberry Pi:
# On Raspberry Pi
git clone https://github.com/sage-app/backend
cd backend
cp .env.example .env
# Edit .env with your settings
docker-compose up -d
📊 METRICS & ANALYTICS
Privacy-Respecting Metrics
What we DON'T track:
- Personal data
- Specific food items
- User behavior
- Location data
What we CAN track (locally, opt-in):
- Items saved (count only, for user's stats)
- Food waste prevented (estimated)
- Money saved (estimated from item costs)
- App usage (locally, for user dashboard)
User Dashboard:
Your Stats (This Month):
- 🛒 24 items tracked
- 💰 Estimated savings: $47
- 🌱 Food waste prevented: 3 lbs
- 📖 Recipes tried: 5
- ⭐ Most used recipe: Caesar Salad
🎨 DESIGN SYSTEM
Color Palette
class AppColors {
// Primary - Sage Green theme
static const primary = Color(0xFF4CAF50);
static const primaryDark = Color(0xFF388E3C);
static const primaryLight = Color(0xFF81C784);
// Expiration Status Colors
static const fresh = Color(0xFF4CAF50); // Green
static const caution = Color(0xFFFFEB3B); // Yellow
static const warning = Color(0xFFFF9800); // Orange
static const critical = Color(0xFFF44336); // Red
static const expired = Color(0xFF9E9E9E); // Gray
// UI
static const background = Color(0xFFFAFAFA);
static const surface = Color(0xFFFFFFFF);
static const text = Color(0xFF212121);
static const textSecondary = Color(0xFF757575);
}
Typography
class AppTextStyles {
static const headline1 = TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppColors.text,
);
static const headline2 = TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.text,
);
static const body = TextStyle(
fontSize: 16,
color: AppColors.text,
);
static const caption = TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
);
}
🔮 FUTURE FEATURES
Phase 8+ (Post-MVP)
-
Meal Planning Calendar
- Plan meals for the week
- Auto-generate shopping list
- Track what you ate
-
Nutrition Tracking
- From Open Food Facts nutrition data
- Track calories, macros (optional)
- Health insights
-
Price Tracking
- Log item costs
- Track spending over time
- Price comparison between stores
-
Voice Input
- "Hey Sage, add milk to my shopping list"
- Voice-activated barcode scanning
-
AI Recipe Suggestions
- "What can I make with chicken and rice?"
- Generate recipes from ingredients
- Dietary restrictions
-
Household Gamification
- Compete to waste less food
- Achievements & badges
- Family leaderboard
-
Integration with Smart Appliances
- Samsung Family Hub fridge
- Smart scales for quantity tracking
Built with 💚 by enthusiastic developers who hate wasting food!
Let's make kitchens smarter, one scan at a time! 🌿