docs: Add comprehensive technical documentation
Complete documentation suite: - DATABASE.md: Full schema, RLS policies, functions, queries - API.md: Supabase client API, Edge functions, realtime - DEVELOPMENT.md: Setup, workflow, conventions, testing - DEPLOYMENT.md: Docker Compose, Coolify, monitoring, backups Ready for development to begin.
This commit is contained in:
678
docs/DEVELOPMENT.md
Normal file
678
docs/DEVELOPMENT.md
Normal file
@@ -0,0 +1,678 @@
|
||||
# Pantry - Development Guide
|
||||
|
||||
**Version:** 1.0
|
||||
**Last Updated:** 2026-02-08
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Node.js** 20+ (or Bun 1.0+)
|
||||
- **Docker** & Docker Compose
|
||||
- **Git**
|
||||
- **Code editor** (VS Code recommended)
|
||||
|
||||
### Clone & Setup
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://gitea.jeanlucmakiola.de/pantry-app/pantry.git
|
||||
cd pantry
|
||||
|
||||
# Install dependencies (using Bun)
|
||||
bun install
|
||||
|
||||
# Copy environment template
|
||||
cp .env.example .env
|
||||
|
||||
# Start Supabase (PostgreSQL + Auth + Realtime)
|
||||
docker-compose -f docker/docker-compose.dev.yml up -d
|
||||
|
||||
# Run database migrations
|
||||
cd supabase
|
||||
supabase db reset # Creates schema + seeds data
|
||||
|
||||
# Start Nuxt dev server
|
||||
cd ../app
|
||||
bun run dev
|
||||
|
||||
# Access app at http://localhost:3000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
pantry/
|
||||
├── app/ # Nuxt 4 frontend
|
||||
│ ├── components/
|
||||
│ │ ├── inventory/ # Inventory UI components
|
||||
│ │ ├── scan/ # Barcode scanner components
|
||||
│ │ ├── tags/ # Tag management
|
||||
│ │ └── common/ # Shared UI components
|
||||
│ ├── composables/ # Shared logic (Vue Composition API)
|
||||
│ │ ├── useBarcode.ts
|
||||
│ │ ├── useInventory.ts
|
||||
│ │ └── useSupabase.ts
|
||||
│ ├── pages/ # Route pages
|
||||
│ │ ├── index.vue # Inventory list
|
||||
│ │ ├── scan.vue # Barcode scanner
|
||||
│ │ └── settings.vue # Settings
|
||||
│ ├── utils/ # Pure functions
|
||||
│ │ ├── conversions.ts # Unit conversion math
|
||||
│ │ └── validation.ts
|
||||
│ ├── nuxt.config.ts # Nuxt configuration
|
||||
│ └── package.json
|
||||
├── supabase/ # Database & backend
|
||||
│ ├── migrations/ # SQL migrations
|
||||
│ │ ├── 001_schema.sql
|
||||
│ │ ├── 002_seed_units.sql
|
||||
│ │ └── 003_rls.sql
|
||||
│ ├── functions/ # Edge functions
|
||||
│ │ └── product-lookup/
|
||||
│ ├── seed/ # Seed data (JSON)
|
||||
│ │ ├── units.json
|
||||
│ │ └── tags.json
|
||||
│ └── config.toml # Supabase config
|
||||
├── docker/ # Docker configs
|
||||
│ ├── docker-compose.dev.yml # Development
|
||||
│ └── docker-compose.prod.yml # Production
|
||||
├── docs/ # Documentation
|
||||
├── scripts/ # Utility scripts
|
||||
│ ├── seed-db.ts
|
||||
│ └── export-schema.ts
|
||||
├── .env.example
|
||||
├── .gitignore
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Development Workflow
|
||||
|
||||
### 1. Create a Feature Branch
|
||||
|
||||
```bash
|
||||
git checkout -b feature/barcode-scanner
|
||||
```
|
||||
|
||||
### 2. Make Changes
|
||||
|
||||
**Add a new component:**
|
||||
```bash
|
||||
# Create component file
|
||||
touch app/components/scan/BarcodeScanner.vue
|
||||
|
||||
# Use in a page
|
||||
# app/pages/scan.vue
|
||||
<template>
|
||||
<BarcodeScanner @scan="handleScan" />
|
||||
</template>
|
||||
```
|
||||
|
||||
**Add a database migration:**
|
||||
```bash
|
||||
cd supabase
|
||||
supabase migration new add_location_field
|
||||
|
||||
# Edit supabase/migrations/XXX_add_location_field.sql
|
||||
ALTER TABLE inventory_items ADD COLUMN location TEXT;
|
||||
|
||||
# Apply locally
|
||||
supabase db reset
|
||||
```
|
||||
|
||||
### 3. Test Locally
|
||||
|
||||
```bash
|
||||
# Run type check
|
||||
bun run typecheck
|
||||
|
||||
# Run linter
|
||||
bun run lint
|
||||
|
||||
# Run tests (when implemented)
|
||||
bun run test
|
||||
|
||||
# E2E tests
|
||||
bun run test:e2e
|
||||
```
|
||||
|
||||
### 4. Commit & Push
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat: Add barcode scanner component"
|
||||
git push origin feature/barcode-scanner
|
||||
```
|
||||
|
||||
### 5. Create Pull Request
|
||||
|
||||
- Go to Gitea: https://gitea.jeanlucmakiola.de/pantry-app/pantry
|
||||
- Create PR from your branch to `main`
|
||||
- Request review
|
||||
- Merge when approved
|
||||
|
||||
---
|
||||
|
||||
## 📝 Code Conventions
|
||||
|
||||
### Naming
|
||||
|
||||
**Files:**
|
||||
- Components: `PascalCase.vue` (e.g., `BarcodeScanner.vue`)
|
||||
- Composables: `camelCase.ts` (e.g., `useBarcode.ts`)
|
||||
- Utils: `camelCase.ts` (e.g., `conversions.ts`)
|
||||
- Pages: `kebab-case.vue` (e.g., `item-detail.vue`)
|
||||
|
||||
**Variables:**
|
||||
- `camelCase` for variables, functions
|
||||
- `PascalCase` for types, interfaces
|
||||
- `SCREAMING_SNAKE_CASE` for constants
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
const itemCount = 5
|
||||
const fetchProducts = () => {}
|
||||
interface Product { ... }
|
||||
const MAX_ITEMS = 100
|
||||
|
||||
// Bad
|
||||
const ItemCount = 5
|
||||
const fetch_products = () => {}
|
||||
interface product { ... }
|
||||
const maxItems = 100 // for constants
|
||||
```
|
||||
|
||||
### Vue Component Structure
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// 1. Imports
|
||||
import { ref, computed } from 'vue'
|
||||
import type { InventoryItem } from '~/types'
|
||||
|
||||
// 2. Props & Emits
|
||||
interface Props {
|
||||
item: InventoryItem
|
||||
editable?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
editable: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
save: [item: InventoryItem]
|
||||
cancel: []
|
||||
}>()
|
||||
|
||||
// 3. Composables
|
||||
const { convert } = useUnits()
|
||||
const supabase = useSupabaseClient()
|
||||
|
||||
// 4. State
|
||||
const quantity = ref(props.item.quantity)
|
||||
const isEditing = ref(false)
|
||||
|
||||
// 5. Computed
|
||||
const displayQuantity = computed(() =>
|
||||
`${quantity.value} ${props.item.unit.abbreviation}`
|
||||
)
|
||||
|
||||
// 6. Methods
|
||||
const handleSave = async () => {
|
||||
const { error } = await supabase
|
||||
.from('inventory_items')
|
||||
.update({ quantity: quantity.value })
|
||||
.eq('id', props.item.id)
|
||||
|
||||
if (!error) emit('save', { ...props.item, quantity: quantity.value })
|
||||
}
|
||||
|
||||
// 7. Lifecycle (if needed)
|
||||
onMounted(() => {
|
||||
console.log('Component mounted')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="item-card">
|
||||
<h3>{{ item.name }}</h3>
|
||||
<p>{{ displayQuantity }}</p>
|
||||
|
||||
<button v-if="editable" @click="handleSave">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item-card {
|
||||
/* Prefer Tailwind classes in template */
|
||||
/* Use scoped styles only for complex/unique styles */
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### TypeScript
|
||||
|
||||
**Prefer interfaces over types:**
|
||||
```typescript
|
||||
// Good
|
||||
interface InventoryItem {
|
||||
id: string
|
||||
name: string
|
||||
quantity: number
|
||||
}
|
||||
|
||||
// Only use type for unions, intersections
|
||||
type Status = 'active' | 'expired'
|
||||
```
|
||||
|
||||
**Use strict typing:**
|
||||
```typescript
|
||||
// Good
|
||||
const fetchItem = async (id: string): Promise<InventoryItem | null> => {
|
||||
const { data } = await supabase
|
||||
.from('inventory_items')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single()
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// Bad (implicit any)
|
||||
const fetchItem = async (id) => {
|
||||
const { data } = await supabase...
|
||||
return data
|
||||
}
|
||||
```
|
||||
|
||||
### Composables
|
||||
|
||||
**Naming:** Always start with `use`
|
||||
|
||||
**Structure:**
|
||||
```typescript
|
||||
// composables/useInventory.ts
|
||||
export function useInventory() {
|
||||
const supabase = useSupabaseClient<Database>()
|
||||
const items = ref<InventoryItem[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const fetchItems = async () => {
|
||||
loading.value = true
|
||||
const { data, error } = await supabase
|
||||
.from('inventory_items')
|
||||
.select('*')
|
||||
|
||||
if (data) items.value = data
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const addItem = async (item: NewInventoryItem) => {
|
||||
const { data, error } = await supabase
|
||||
.from('inventory_items')
|
||||
.insert(item)
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if (data) items.value.push(data)
|
||||
return { data, error }
|
||||
}
|
||||
|
||||
// Return reactive state + methods
|
||||
return {
|
||||
items: readonly(items),
|
||||
loading: readonly(loading),
|
||||
fetchItems,
|
||||
addItem
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Database Migrations
|
||||
|
||||
**Naming:**
|
||||
```
|
||||
001_initial_schema.sql
|
||||
002_seed_defaults.sql
|
||||
003_add_location_field.sql
|
||||
```
|
||||
|
||||
**Structure:**
|
||||
```sql
|
||||
-- Migration: Add location field to inventory items
|
||||
-- Created: 2026-02-08
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE inventory_items
|
||||
ADD COLUMN location TEXT;
|
||||
|
||||
-- Update existing items (optional)
|
||||
UPDATE inventory_items
|
||||
SET location = 'Pantry'
|
||||
WHERE location IS NULL;
|
||||
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
**Rollback (optional):**
|
||||
```sql
|
||||
-- To rollback:
|
||||
-- ALTER TABLE inventory_items DROP COLUMN location;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Unit Tests (Vitest)
|
||||
|
||||
```typescript
|
||||
// app/utils/conversions.test.ts
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { convertUnit } from './conversions'
|
||||
|
||||
describe('convertUnit', () => {
|
||||
it('converts grams to kilograms', () => {
|
||||
const result = convertUnit(500, {
|
||||
conversion_factor: 0.001,
|
||||
base_unit_id: 'kg'
|
||||
}, {
|
||||
conversion_factor: 1,
|
||||
base_unit_id: null
|
||||
})
|
||||
|
||||
expect(result).toBe(0.5)
|
||||
})
|
||||
|
||||
it('throws error for incompatible units', () => {
|
||||
expect(() => {
|
||||
convertUnit(1,
|
||||
{ unit_type: 'weight', conversion_factor: 1 },
|
||||
{ unit_type: 'volume', conversion_factor: 1 }
|
||||
)
|
||||
}).toThrow('Cannot convert')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Run tests:**
|
||||
```bash
|
||||
bun test
|
||||
bun test --watch # Watch mode
|
||||
```
|
||||
|
||||
### E2E Tests (Playwright)
|
||||
|
||||
```typescript
|
||||
// tests/e2e/inventory.spec.ts
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('can add item to inventory', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
|
||||
// Login
|
||||
await page.fill('[name=email]', 'test@example.com')
|
||||
await page.fill('[name=password]', 'password')
|
||||
await page.click('button[type=submit]')
|
||||
|
||||
// Add item
|
||||
await page.click('[data-testid=add-item]')
|
||||
await page.fill('[name=name]', 'Test Item')
|
||||
await page.fill('[name=quantity]', '2')
|
||||
await page.click('button:has-text("Add")')
|
||||
|
||||
// Verify
|
||||
await expect(page.locator('text=Test Item')).toBeVisible()
|
||||
})
|
||||
```
|
||||
|
||||
**Run E2E:**
|
||||
```bash
|
||||
bun run test:e2e
|
||||
bun run test:e2e --ui # Interactive mode
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Supabase Local Development
|
||||
|
||||
### Start Supabase
|
||||
|
||||
```bash
|
||||
cd supabase
|
||||
supabase start
|
||||
```
|
||||
|
||||
**Outputs:**
|
||||
```
|
||||
API URL: http://localhost:54321
|
||||
Studio URL: http://localhost:54323
|
||||
Anon key: eyJhb...
|
||||
Service key: eyJhb...
|
||||
```
|
||||
|
||||
**Access Supabase Studio:**
|
||||
- Open http://localhost:54323
|
||||
- View tables, run queries, manage auth
|
||||
|
||||
### Apply Migrations
|
||||
|
||||
```bash
|
||||
# Reset DB (drops all data, reapplies migrations)
|
||||
supabase db reset
|
||||
|
||||
# Create new migration
|
||||
supabase migration new add_feature
|
||||
|
||||
# Apply migrations (non-destructive)
|
||||
supabase migration up
|
||||
```
|
||||
|
||||
### Generate Types
|
||||
|
||||
```bash
|
||||
# Generate TypeScript types from database schema
|
||||
supabase gen types typescript --local > app/types/supabase.ts
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```typescript
|
||||
import type { Database } from '~/types/supabase'
|
||||
|
||||
const supabase = useSupabaseClient<Database>()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Environment Variables
|
||||
|
||||
### `.env` (Development)
|
||||
|
||||
```bash
|
||||
# Supabase (from `supabase start`)
|
||||
SUPABASE_URL=http://localhost:54321
|
||||
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
|
||||
# App
|
||||
PUBLIC_APP_URL=http://localhost:3000
|
||||
|
||||
# Open Food Facts (optional, no key needed)
|
||||
OPENFOODFACTS_API_URL=https://world.openfoodfacts.org
|
||||
```
|
||||
|
||||
### `.env.production`
|
||||
|
||||
```bash
|
||||
# Supabase (production)
|
||||
SUPABASE_URL=https://your-project.supabase.co
|
||||
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
|
||||
# App
|
||||
PUBLIC_APP_URL=https://pantry.yourdomain.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Debugging
|
||||
|
||||
### Nuxt DevTools
|
||||
|
||||
**Enable:**
|
||||
```typescript
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
devtools: { enabled: true }
|
||||
})
|
||||
```
|
||||
|
||||
**Access:** Press `Shift + Alt + D` or visit http://localhost:3000/__devtools__
|
||||
|
||||
### Vue DevTools
|
||||
|
||||
Install browser extension:
|
||||
- Chrome: [Vue DevTools](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||
- Firefox: [Vue DevTools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||
|
||||
### Supabase Logs
|
||||
|
||||
```bash
|
||||
# View realtime logs
|
||||
supabase logs --tail
|
||||
|
||||
# Filter by service
|
||||
supabase logs --service postgres
|
||||
supabase logs --service auth
|
||||
```
|
||||
|
||||
### Database Queries
|
||||
|
||||
**Supabase Studio:**
|
||||
- Open http://localhost:54323
|
||||
- Go to "SQL Editor"
|
||||
- Run queries directly
|
||||
|
||||
**CLI:**
|
||||
```bash
|
||||
# psql into local database
|
||||
supabase db shell
|
||||
|
||||
# Run query
|
||||
SELECT * FROM inventory_items LIMIT 5;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Build & Preview
|
||||
|
||||
### Build for Production
|
||||
|
||||
```bash
|
||||
cd app
|
||||
bun run build
|
||||
```
|
||||
|
||||
**Output:** `.output/` directory (ready for deployment)
|
||||
|
||||
### Preview Production Build
|
||||
|
||||
```bash
|
||||
bun run preview
|
||||
```
|
||||
|
||||
**Access:** http://localhost:3000
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Git Workflow
|
||||
|
||||
### Branch Naming
|
||||
|
||||
```
|
||||
feature/barcode-scanner
|
||||
fix/tag-duplication-bug
|
||||
chore/update-dependencies
|
||||
docs/api-reference
|
||||
```
|
||||
|
||||
### Commit Messages (Conventional Commits)
|
||||
|
||||
```bash
|
||||
# Format: <type>(<scope>): <subject>
|
||||
|
||||
feat(scan): add barcode detection
|
||||
fix(inventory): prevent duplicate items
|
||||
chore(deps): update Nuxt to 4.1
|
||||
docs(api): add product lookup endpoint
|
||||
refactor(tags): simplify tag picker logic
|
||||
test(units): add conversion edge cases
|
||||
style(ui): apply Tailwind spacing
|
||||
```
|
||||
|
||||
**Types:**
|
||||
- `feat`: New feature
|
||||
- `fix`: Bug fix
|
||||
- `chore`: Maintenance (deps, config)
|
||||
- `docs`: Documentation
|
||||
- `refactor`: Code restructuring
|
||||
- `test`: Adding/updating tests
|
||||
- `style`: Code style (formatting, no logic change)
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Common Issues
|
||||
|
||||
### Supabase won't start
|
||||
|
||||
```bash
|
||||
# Check Docker
|
||||
docker ps
|
||||
|
||||
# Restart Supabase
|
||||
supabase stop
|
||||
supabase start
|
||||
```
|
||||
|
||||
### Types out of sync
|
||||
|
||||
```bash
|
||||
# Regenerate types after schema change
|
||||
supabase gen types typescript --local > app/types/supabase.ts
|
||||
```
|
||||
|
||||
### Port already in use
|
||||
|
||||
```bash
|
||||
# Nuxt (3000)
|
||||
lsof -ti:3000 | xargs kill
|
||||
|
||||
# Supabase (54321)
|
||||
supabase stop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Resources
|
||||
|
||||
**Docs:**
|
||||
- [Nuxt 4](https://nuxt.com)
|
||||
- [Supabase](https://supabase.com/docs)
|
||||
- [Vue 3](https://vuejs.org)
|
||||
- [Tailwind CSS](https://tailwindcss.com)
|
||||
|
||||
**Community:**
|
||||
- [Pantry Discussions](https://gitea.jeanlucmakiola.de/pantry-app/pantry/issues)
|
||||
- [Nuxt Discord](https://discord.com/invite/ps2h6QT)
|
||||
- [Supabase Discord](https://discord.supabase.com)
|
||||
|
||||
---
|
||||
|
||||
**Next:** [Deployment Guide](./DEPLOYMENT.md)
|
||||
Reference in New Issue
Block a user