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.
13 KiB
13 KiB
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
# 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
git checkout -b feature/barcode-scanner
2. Make Changes
Add a new component:
# 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:
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
# 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
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:
camelCasefor variables, functionsPascalCasefor types, interfacesSCREAMING_SNAKE_CASEfor constants
// 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
<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:
// Good
interface InventoryItem {
id: string
name: string
quantity: number
}
// Only use type for unions, intersections
type Status = 'active' | 'expired'
Use strict typing:
// 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:
// 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:
-- 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):
-- To rollback:
-- ALTER TABLE inventory_items DROP COLUMN location;
🧪 Testing
Unit Tests (Vitest)
// 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:
bun test
bun test --watch # Watch mode
E2E Tests (Playwright)
// 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:
bun run test:e2e
bun run test:e2e --ui # Interactive mode
🔧 Supabase Local Development
Start Supabase
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
# 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
# Generate TypeScript types from database schema
supabase gen types typescript --local > app/types/supabase.ts
Usage:
import type { Database } from '~/types/supabase'
const supabase = useSupabaseClient<Database>()
🌐 Environment Variables
.env (Development)
# 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
# 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:
// 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
- Firefox: Vue DevTools
Supabase Logs
# 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:
# psql into local database
supabase db shell
# Run query
SELECT * FROM inventory_items LIMIT 5;
📦 Build & Preview
Build for Production
cd app
bun run build
Output: .output/ directory (ready for deployment)
Preview Production Build
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)
# 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 featurefix: Bug fixchore: Maintenance (deps, config)docs: Documentationrefactor: Code restructuringtest: Adding/updating testsstyle: Code style (formatting, no logic change)
🚨 Common Issues
Supabase won't start
# Check Docker
docker ps
# Restart Supabase
supabase stop
supabase start
Types out of sync
# Regenerate types after schema change
supabase gen types typescript --local > app/types/supabase.ts
Port already in use
# Nuxt (3000)
lsof -ti:3000 | xargs kill
# Supabase (54321)
supabase stop
📚 Resources
Docs:
Community:
Next: Deployment Guide