docs: Initial project documentation

- README: Project overview, quick start, tech stack
- PROJECT_PLAN: Vision, roadmap, 6-week MVP timeline
- ARCHITECTURE: System design, data model, tech decisions

Establishes foundation for Pantry - self-hosted kitchen
inventory PWA with barcode scanning.
This commit is contained in:
Claw
2026-02-08 18:47:48 +00:00
commit 41537fa84c
3 changed files with 1078 additions and 0 deletions

655
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,655 @@
# Pantry - Architecture
**Version:** 1.0
**Last Updated:** 2026-02-08
---
## 🏗️ System Overview
```
┌─────────────────────────────────────────────┐
│ User Devices │
│ (Phone, Tablet, Desktop - PWA) │
└──────────────────┬──────────────────────────┘
│ HTTPS
┌─────────────────────────────────────────────┐
│ Nuxt 4 Frontend (SSR) │
│ - Vue 3 Components │
│ - Tailwind CSS + Nuxt UI │
│ - PWA (offline-first) │
│ - Barcode Scanner │
└──────────────────┬──────────────────────────┘
│ WebSocket + REST
┌─────────────────────────────────────────────┐
│ Supabase Platform │
│ ┌─────────────────────────────────────┐ │
│ │ PostgreSQL (Database) │ │
│ │ - Items, Products, Tags, Units │ │
│ │ - Row Level Security (RLS) │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ Auth (GoTrue) │ │
│ │ - Email/Password │ │
│ │ - OIDC (optional) │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ Realtime (WebSocket) │ │
│ │ - Live updates across devices │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ Edge Functions (Deno) │ │
│ │ - Product lookup │ │
│ │ - Barcode cache │ │
│ └─────────────────────────────────────┘ │
└──────────────────┬──────────────────────────┘
│ HTTP
┌─────────────────────────────────────────────┐
│ Open Food Facts API │
│ (External - product data) │
└─────────────────────────────────────────────┘
```
---
## 📦 Tech Stack
### Frontend
**Nuxt 4** (Vue 3)
- **Why:** Meta-framework, SSR, excellent DX
- **Alternatives considered:** Next.js (React), SvelteKit
- **Decision:** Vue ecosystem maturity + Nuxt UI
**Tailwind CSS**
- **Why:** Utility-first, fast iteration
- **Alternatives:** CSS Modules, UnoCSS
- **Decision:** Industry standard, Nuxt UI built on it
**Nuxt UI**
- **Why:** Pre-built components, accessible
- **Alternatives:** Headless UI, Shadcn
- **Decision:** First-party Nuxt support
**html5-qrcode**
- **Why:** PWA camera support, multi-format
- **Alternatives:** ZXing, QuaggaJS
- **Decision:** Best mobile performance in tests
### Backend
**Supabase**
- **Why:** Postgres + Auth + Realtime in one
- **Alternatives:** Firebase, Appwrite, custom backend
- **Decision:** Self-hosted, SQL flexibility, mature
**PostgreSQL 15+**
- **Why:** Robust, supports JSONB, full-text search
- **Alternatives:** MySQL, MongoDB
- **Decision:** Comes with Supabase, best for relational data
**Deno Edge Functions**
- **Why:** Serverless, TypeScript-native
- **Alternatives:** Node.js functions, custom API
- **Decision:** Part of Supabase, fast cold starts
### Deployment
**Docker Compose**
- **Why:** Simple multi-container orchestration
- **Alternatives:** Kubernetes, bare metal
- **Decision:** Right complexity level for self-hosted
**Bun** (runtime)
- **Why:** Fast, npm-compatible, built-in TypeScript
- **Alternatives:** Node.js, pnpm
- **Decision:** Modern, faster than Node
---
## 🗄️ Data Model
### Core Entities
```mermaid
erDiagram
INVENTORY_ITEMS ||--o| PRODUCTS : references
INVENTORY_ITEMS }o--o{ TAGS : tagged_with
INVENTORY_ITEMS ||--|| UNITS : uses
PRODUCTS ||--|| UNITS : default_unit
UNITS ||--o| UNITS : converts_to
ITEM_TAGS }|--|| INVENTORY_ITEMS : item
ITEM_TAGS }|--|| TAGS : tag
INVENTORY_ITEMS {
uuid id PK
uuid product_id FK
string name
decimal quantity
uuid unit_id FK
date expiry_date
uuid added_by FK
timestamp created_at
timestamp updated_at
}
PRODUCTS {
uuid id PK
string barcode UK
string name
string brand
string image_url
uuid default_unit_id FK
timestamp cached_at
}
TAGS {
uuid id PK
string name
enum category
string icon
string color
uuid created_by FK
}
UNITS {
uuid id PK
string name
string abbreviation
enum unit_type
uuid base_unit_id FK
decimal conversion_factor
boolean is_default
uuid created_by FK
}
ITEM_TAGS {
uuid item_id FK
uuid tag_id FK
}
```
### Key Relationships
1. **Items ↔ Products:** Many-to-one (multiple items from same product)
2. **Items ↔ Tags:** Many-to-many (items can have multiple tags)
3. **Units ↔ Units:** Self-referential (g → kg conversion)
4. **Items → Users:** Audit trail (who added)
---
## 🔐 Security Model
### Authentication
**Supabase Auth (GoTrue)**
**Email/Password (Default):**
```typescript
const { user, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secure-password'
})
```
**OIDC (Optional - Admin Config):**
```yaml
# Admin sets via environment
SUPABASE_AUTH_GOOGLE_ENABLED=true
SUPABASE_AUTH_GOOGLE_CLIENT_ID=...
SUPABASE_AUTH_GOOGLE_SECRET=...
```
### Authorization
**Row Level Security (RLS)**
**Inventory Items:**
```sql
-- Everyone can read (shared inventory)
CREATE POLICY "items_select" ON inventory_items
FOR SELECT USING (true);
-- Authenticated users can insert
CREATE POLICY "items_insert" ON inventory_items
FOR INSERT WITH CHECK (
auth.uid() IS NOT NULL
);
-- Users can update any item (trust model)
CREATE POLICY "items_update" ON inventory_items
FOR UPDATE USING (
auth.uid() IS NOT NULL
);
-- Users can delete any item
CREATE POLICY "items_delete" ON inventory_items
FOR DELETE USING (
auth.uid() IS NOT NULL
);
```
**Products (read-only for users):**
```sql
-- Everyone reads
CREATE POLICY "products_select" ON products
FOR SELECT USING (true);
-- Only service role writes (via Edge Functions)
-- (No user-facing policy needed)
```
**Tags (user-managed):**
```sql
-- Default tags: everyone reads
-- Custom tags: creator manages
CREATE POLICY "tags_select" ON tags
FOR SELECT USING (true);
CREATE POLICY "tags_insert" ON tags
FOR INSERT WITH CHECK (
auth.uid() IS NOT NULL
);
CREATE POLICY "tags_update" ON tags
FOR UPDATE USING (
created_by = auth.uid() OR created_by IS NULL
);
```
---
## 🔄 Data Flow
### 1. Barcode Scan Flow
```
User taps "Scan" button
Camera permission requested (if first time)
Camera opens (html5-qrcode)
Barcode detected (e.g., "8000500310427")
Frontend calls Edge Function
POST /functions/v1/product-lookup
{ barcode: "8000500310427" }
Edge Function checks products table
├─ Found: return cached product
└─ Not found:
Fetch from Open Food Facts API
https://world.openfoodfacts.org/api/v2/product/{barcode}
Parse response (name, brand, image, etc.)
Cache in products table
Return product data
Frontend displays product card
User sets quantity, tags, expiry (optional)
Frontend inserts into inventory_items table
Realtime updates other devices (WebSocket)
Done!
```
### 2. Manual Add Flow
```
User taps "Add Manually"
Form: name, quantity, unit, tags, expiry
Frontend validates (required: name, quantity)
Insert into inventory_items
Realtime broadcast to other devices
Done!
```
### 3. Unit Conversion Flow
```
User selects unit (e.g., g)
Item has quantity (e.g., 500g)
User wants to view in kg
Frontend calls conversion logic:
convert(500, g, kg)
Find g.base_unit = kg
Find g.conversion_factor = 0.001
result = 500 * 0.001 = 0.5 kg
Display: "0.5 kg (500 g)"
```
---
## 🌐 API Design
### Supabase Client (Direct Access)
**Inventory CRUD:**
```typescript
// List items
const { data } = await supabase
.from('inventory_items')
.select(`
*,
product:products(*),
unit:units(*),
tags:item_tags(tag:tags(*))
`)
.order('created_at', { ascending: false })
// Add item
const { data, error } = await supabase
.from('inventory_items')
.insert({
product_id: '...',
quantity: 1,
unit_id: '...',
added_by: user.id
})
// Update quantity (consume)
const { data } = await supabase
.from('inventory_items')
.update({ quantity: item.quantity - 1 })
.eq('id', itemId)
// Delete item
const { error } = await supabase
.from('inventory_items')
.delete()
.eq('id', itemId)
```
### Edge Functions
**Product Lookup:**
```typescript
// POST /functions/v1/product-lookup
// Body: { barcode: string }
interface Request {
barcode: string
}
interface Response {
product: Product | null
cached: boolean
}
// Returns cached product or fetches from Open Food Facts
```
**Batch Lookup (future):**
```typescript
// POST /functions/v1/products/batch
// Body: { barcodes: string[] }
// For offline sync (scan multiple, upload batch)
```
---
## 📱 PWA Architecture
### Service Worker Strategy
**Network-first with cache fallback:**
```typescript
// App shell: cache-first
self.addEventListener('fetch', (event) => {
if (event.request.url.includes('/app')) {
event.respondWith(
caches.match(event.request)
.then(cached => cached || fetch(event.request))
)
}
})
// API calls: network-first
if (event.request.url.includes('/api')) {
event.respondWith(
fetch(event.request)
.catch(() => caches.match(event.request))
)
}
```
### Offline Support
**Offline Queue:**
```typescript
// Queue scans when offline
if (!navigator.onLine) {
await db.offlineQueue.add({
type: 'scan',
barcode: '123456',
timestamp: Date.now()
})
}
// Sync when online
window.addEventListener('online', async () => {
const queue = await db.offlineQueue.getAll()
for (const item of queue) {
await processQueueItem(item)
}
})
```
---
## 🎨 Frontend Architecture
### Component Structure
```
app/
├── components/
│ ├── inventory/
│ │ ├── ItemList.vue # Main inventory view
│ │ ├── ItemCard.vue # Single item card
│ │ ├── QuickActions.vue # Consume, restock buttons
│ │ └── FilterBar.vue # Search & filter
│ ├── scan/
│ │ ├── BarcodeScanner.vue # Camera + detection
│ │ ├── ScanButton.vue # Floating action button
│ │ └── ProductCard.vue # After scan confirmation
│ ├── tags/
│ │ ├── TagPicker.vue # Multi-select tags
│ │ ├── TagBadge.vue # Display tag
│ │ └── TagManager.vue # Settings: manage tags
│ ├── units/
│ │ ├── UnitSelector.vue # Dropdown with conversions
│ │ └── UnitDisplay.vue # Show quantity + unit
│ └── common/
│ ├── AppHeader.vue
│ ├── AppFooter.vue
│ └── LoadingState.vue
├── composables/
│ ├── useBarcode.ts # Camera, detection logic
│ ├── useInventory.ts # CRUD operations
│ ├── useUnits.ts # Conversions
│ ├── useTags.ts # Tag filtering
│ └── useOpenFoodFacts.ts # API wrapper
├── pages/
│ ├── index.vue # Inventory list
│ ├── scan.vue # Full-screen scanner
│ ├── item/[id].vue # Item detail/edit
│ └── settings.vue # Tags, units, profile
└── utils/
├── conversions.ts # Unit conversion math
├── barcode-formats.ts # Supported formats
└── offline-queue.ts # Offline sync
```
### State Management
**No Vuex/Pinia needed (for MVP):**
- Supabase Realtime = reactive state
- Composables for shared logic
- Props/emits for component state
**If needed later:**
- Pinia for complex UI state (filters, etc.)
---
## 🔧 Development Workflow
### Local Development
```bash
# Terminal 1: Supabase
docker-compose -f docker/docker-compose.dev.yml up
# Terminal 2: Nuxt dev server
cd app
bun run dev
# Access:
# - App: http://localhost:3000
# - Supabase Studio: http://localhost:54323
```
### Database Migrations
```bash
# Create migration
supabase migration new add_tags_table
# Apply locally
supabase db reset
# Push to production
supabase db push
```
### Testing
```bash
# Unit tests
bun test
# E2E tests (Playwright)
bun run test:e2e
# Type check
bun run typecheck
```
---
## 🚀 Deployment
### Docker Compose (Production)
```yaml
services:
supabase:
image: supabase/postgres:15
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
supabase-auth:
image: supabase/gotrue
environment:
SITE_URL: ${SITE_URL}
# OIDC config (optional)
pantry:
build: ./app
environment:
SUPABASE_URL: http://supabase:8000
SUPABASE_ANON_KEY: ${SUPABASE_ANON_KEY}
ports:
- "3000:3000"
```
### Coolify Deployment
**Service Config:**
- Type: Docker Compose
- Repo: pantry-app/pantry
- Compose File: `docker/docker-compose.yml`
- Domain: `pantry.yourdomain.com`
---
## 📊 Performance Considerations
### Image Optimization
- Open Food Facts images cached locally
- Nuxt Image module for responsive images
- WebP format with fallbacks
### Database Indexing
```sql
-- Fast barcode lookup
CREATE INDEX idx_products_barcode ON products(barcode);
-- Fast tag filtering
CREATE INDEX idx_item_tags_tag ON item_tags(tag_id);
-- Fast user queries
CREATE INDEX idx_items_added_by ON inventory_items(added_by);
```
### Realtime Optimization
- Only subscribe to user's inventory (RLS filters)
- Debounce updates (don't broadcast every keystroke)
---
## 🔮 Future Considerations
### Scalability
- **Database:** Postgres scales to millions of rows
- **Storage:** Images stored in Supabase Storage (S3-compatible)
- **Caching:** Redis for product lookup cache (if Open Food Facts is slow)
### Multi-Tenancy (v2.0+)
- Add `household_id` to all tables
- RLS policies filter by household
- Invite system for family members
### Native Mobile App (v2.0+)
- Capacitor wrapper around Nuxt
- Native barcode scanner (faster than PWA)
- Push notifications (expiry alerts)
---
## 📚 References
- [Supabase Docs](https://supabase.com/docs)
- [Nuxt 4 Docs](https://nuxt.com)
- [Open Food Facts API](https://wiki.openfoodfacts.org/API)
- [html5-qrcode](https://github.com/mebjas/html5-qrcode)
---
**Next:** [Database Schema](./DATABASE.md)

323
docs/PROJECT_PLAN.md Normal file
View File

@@ -0,0 +1,323 @@
# Pantry - Project Plan
**Version:** 1.0
**Last Updated:** 2026-02-08
**Status:** Planning
---
## 🎯 Vision & Goals
### Problem Statement
Kitchen inventory management is either:
- Too complex (Grocy) — intimidating for casual users
- Too simple (KitchenOwl) — missing critical features
**Gap:** No self-hosted solution that's both powerful AND easy enough for the whole family.
### Solution
**Pantry** — A PWA that makes inventory management invisible:
- **Barcode scanning** reduces data entry to 3 taps
- **Tag-based** organization (no rigid categories)
- **Unit conversions** handled automatically
- **Multi-user** without household complexity
### Success Criteria
- [ ] Non-technical family member can add items solo
- [ ] Adding via barcode takes <10 seconds
- [ ] Zero onboarding docs needed (self-explanatory UI)
- [ ] Deployed and used in production by v0.2
---
## 🎨 Core Principles
### 1. Family-Friendly First
**Test:** Can your parents use it without help?
- No technical jargon in UI
- Defaults for everything
- One primary action per screen
- Forgiving (easy undo)
### 2. Barcode-First Workflow
**Most common action should be fastest:**
```
Scan → Confirm → Done (3 taps)
```
Everything else is secondary.
### 3. Progressive Disclosure
**Simple by default, powerful when needed:**
- Quick add: just quantity
- Advanced: tags, expiry, custom units
- Settings: hidden until needed
### 4. Tag Everything
**No rigid hierarchies:**
- Position tags: fridge, freezer, pantry
- Type tags: dairy, meat, vegan
- Custom tags: meal-prep, groceries-to-buy
- Multi-tag items (dairy + fridge)
### 5. Self-Hosted Only
**No SaaS plans, ever:**
- One-click Docker Compose deploy
- All data stays local
- No phone-home analytics
- MIT licensed
---
## 🗺️ Roadmap
### MVP (v0.1) — 6 weeks
**Target:** Usable for single household
**Core features:**
- ✅ Barcode scanning (PWA camera)
- ✅ Auto-fill from Open Food Facts
- ✅ Tag-based organization
- ✅ Unit conversions (metric defaults)
- ✅ Multi-user (email/password auth)
- ✅ PWA (installable, offline-ready)
**Out of scope for v0.1:**
- OIDC providers (added later)
- Shopping lists
- Recipe integration
- Expiry notifications
- Mobile app (PWA only)
### v0.2 — Polish (4 weeks)
- Quick actions (consume, restock)
- Search & advanced filters
- Expiry date tracking
- Low-stock alerts
- OIDC auth (Authentik, Google)
### v0.3 — Expansion (TBD)
- Shopping list generation
- Recipe integration
- Meal planning
- Barcode printer labels
- Import/export
### v1.0 — Production-Ready (TBD)
- Full test coverage
- Performance optimization
- Mobile app (React Native/Capacitor)
- Admin dashboard
- Multi-language support
---
## 📅 MVP Timeline (6 Weeks)
### Week 1: Foundation
**Goal:** Core infrastructure + auth
- [x] Create organization + monorepo
- [ ] Nuxt 4 scaffold (Tailwind + Nuxt UI)
- [ ] Supabase local setup (Docker)
- [ ] Database schema (initial)
- [ ] Email/password auth
- [ ] Deploy to development environment
**Deliverable:** Authenticated empty app
### Week 2: Core Inventory
**Goal:** Manual inventory management
- [ ] Database migrations (items, tags, units)
- [ ] Seed default tags & units
- [ ] Item CRUD UI
- [ ] Tag picker component
- [ ] Unit selector with conversions
- [ ] Inventory list view
**Deliverable:** Can add/edit items manually
### Week 3: Barcode Scanning
**Goal:** Scan → Add workflow
- [ ] PWA camera permissions
- [ ] Barcode scanner component (html5-qrcode)
- [ ] Open Food Facts API integration
- [ ] Product lookup & cache
- [ ] Scan UI flow
- [ ] Quick-add from scan
**Deliverable:** Barcode scanning works end-to-end
### Week 4: Tag System
**Goal:** Flexible organization
- [ ] Tag management UI
- [ ] Tag categories (position, type, custom)
- [ ] Multi-tag selection
- [ ] Filter by tags
- [ ] Tag-based search
- [ ] Tag statistics
**Deliverable:** Full tag system functional
### Week 5: PWA & Offline
**Goal:** Mobile experience
- [ ] PWA manifest
- [ ] Service worker (offline cache)
- [ ] Install prompt
- [ ] Mobile UI polish
- [ ] Touch gestures
- [ ] Loading states
**Deliverable:** Installable PWA with offline support
### Week 6: Deployment
**Goal:** Production-ready
- [ ] Docker Compose production config
- [ ] Environment variables
- [ ] Coolify deployment guide
- [ ] Backup strategy
- [ ] Monitoring setup
- [ ] User documentation
**Deliverable:** Running in production
---
## 🏗️ Architecture Decisions
### Monorepo Structure
**Decision:** Single repo with `/app` and `/supabase`
**Rationale:**
- Simpler deployment (one repo clone)
- Easier version coordination
- Less overhead for solo/small team
### Supabase Direct Access
**Decision:** Frontend talks to Supabase directly (no custom backend)
**Rationale:**
- Supabase handles auth, RLS, realtime
- Edge functions for complex logic only
- Faster development
- Less infrastructure
### Tag-Based Organization
**Decision:** Unified tags (no separate locations/categories)
**Rationale:**
- More flexible (dairy + fridge + meal-prep)
- Easier to expand (just add tags)
- Simpler data model
- Better search/filter
### Unit Conversions
**Decision:** Base units + conversion factors
**Rationale:**
- Handles metric conversions (kg ↔ g)
- Supports custom units (can, jar)
- User-friendly (pick any unit, we convert)
- Grocy-style but simpler
### Multi-User Without Households
**Decision:** Shared inventory, no tenant isolation
**Rationale:**
- Simpler for single family use
- Trust model (users can edit anything)
- Avoid SaaS complexity
- Can add later if needed
---
## 🧪 Testing Strategy
### Manual Testing (v0.1)
- Playwright E2E tests (critical paths)
- Mobile testing (real devices)
- Barcode scanning (various formats)
### Automated Testing (v0.2+)
- Unit tests (conversion logic, etc.)
- Component tests (Vue Test Utils)
- API tests (Supabase functions)
### User Testing (v0.3+)
- Family members (non-technical)
- Real inventory data
- Feedback loop
---
## 📊 Success Metrics
### MVP Launch (v0.1)
- [ ] 100+ items scanned successfully
- [ ] <10s average scan-to-add time
- [ ] Zero crashes in 1 week of use
- [ ] Positive feedback from 3+ users
### v1.0 Goals
- 10+ active deployments (GitHub issues as proxy)
- 100+ GitHub stars
- Positive user testimonials
- No major bugs >1 week old
---
## 🚧 Known Challenges
### 1. Barcode Quality
**Problem:** Low-light, damaged labels
**Mitigation:** Fallback to manual barcode entry
### 2. Open Food Facts Coverage
**Problem:** Not all products in database
**Mitigation:** Allow manual product creation
### 3. Unit Conversion Edge Cases
**Problem:** Custom units (e.g., "1 can")
**Mitigation:** Store base quantity, let user define conversions
### 4. PWA Install Friction
**Problem:** iOS Safari limitations
**Mitigation:** Clear install instructions, test on iOS
---
## 🔄 Review & Iteration
**Weekly Review:**
- What shipped?
- What blocked us?
- Adjust next week's scope
**Post-MVP:**
- User feedback sessions
- GitHub issues prioritization
- Feature roadmap votes
---
## 📚 References
- [Architecture](./ARCHITECTURE.md)
- [Database Schema](./DATABASE.md)
- [API Reference](./API.md)
- [Development Guide](./DEVELOPMENT.md)
---
**Next Steps:** Review this plan, then build [ARCHITECTURE.md](./ARCHITECTURE.md)