docs: restructure documentation into organized folders
Some checks failed
Deploy to Coolify / Code Quality (pull_request) Has been cancelled
Deploy to Coolify / Run Tests (pull_request) Has been cancelled
Deploy to Coolify / Deploy to Development (pull_request) Has been cancelled
Deploy to Coolify / Deploy to Production (pull_request) Has been cancelled
Deploy to Coolify / Deploy to Test (pull_request) Has been cancelled
Pull Request Checks / Validate PR (pull_request) Has been cancelled

Organized docs into logical subdirectories:

**New Structure:**
- docs/
  - README.md (index with quick links)
  - PROJECT_PLAN.md (root level - main roadmap)
  - development/
    - getting-started.md (5-min quickstart)
    - local-setup.md (detailed Docker Compose guide)
    - workflow.md (daily development)
    - git-workflow.md (branching strategy)
  - architecture/
    - overview.md (tech stack, design)
    - database.md (schema, RLS, migrations)
    - api.md (endpoints, functions)
  - deployment/
    - production.md (Docker, Coolify)
    - ci-cd.md (automated pipelines)

**Cleaned Up:**
- Moved DEV_SETUP.md → docs/development/local-setup.md
- Removed outdated SETUP.md (referenced old Coolify setup)
- Replaced with getting-started.md (current Docker Compose flow)
- Updated README.md links to new structure

All paths tested, no broken links.
This commit is contained in:
Pantry Lead Agent
2026-02-09 13:45:57 +00:00
parent 12bda4c08f
commit b1ef7e43be
12 changed files with 280 additions and 220 deletions

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)