A terroir-first, privacy-respecting wine journal and recommender.
- Web: Next.js 16, TypeScript, Tailwind CSS, shadcn/ui
- Backend: Supabase (PostgreSQL 15, Auth, Edge Functions, Storage, pgvector)
- iOS: SwiftUI (iOS 17+), MapKit
- Android: Kotlin, Jetpack Compose, Material 3
- Secrets: Doppler
- CI/CD: GitHub Actions, Vercel
- AI: OpenAI GPT-4o (label extraction), Jina CLIP v1 (visual embeddings), Supabase gte-small (text embeddings)
- Node.js 22+
- pnpm 11+
- Docker (for local Supabase)
- Supabase CLI:
brew install supabase/tap/supabase - Doppler CLI:
brew install dopplerhq/cli/doppler
# Install dependencies
pnpm install
# Start local Supabase (Postgres, Auth, Storage, Edge Runtime)
supabase start
# Reset database, apply migrations, seed data
supabase db reset
# Sync API keys from Doppler for local edge functions
./scripts/sync-env-local.sh
# Start the dev server
pnpm devOpen http://localhost:3000.
All secrets are managed in Doppler under the vinho project. The pnpm dev command injects them automatically via doppler run.
| Variable | Where used | Description |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Web app (client) | Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Web app (client) | Supabase anonymous/public key |
VINHO_SERVICE_ROLE_KEY |
Edge functions | Supabase service role key (bypasses RLS) |
OPENAI_API_KEY |
Edge functions | GPT-4o for wine label extraction and enrichment |
JINA_API_KEY |
Edge functions | Jina CLIP v1 for visual label embeddings |
NEXT_PUBLIC_POSTHOG_KEY |
Web app (client) | PostHog analytics key |
NEXT_PUBLIC_POSTHOG_HOST |
Web app (client) | PostHog API host |
RESEND_API_KEY |
Edge functions | Resend email delivery |
RESEND_FROM_EMAIL |
Edge functions | Sender email address |
GOOGLE_MAPS_API_KEY |
Web app | Google Maps for location features |
HCAPTCHA_SITE_KEY |
Web app (client) | hCaptcha site key |
HCAPTCHA_SECRET_KEY |
Web app (server) | hCaptcha verification key |
| Config | Environment | Used by |
|---|---|---|
dev |
Local development | pnpm dev, sync-env-local.sh |
stg |
Staging/preview | Vercel preview deployments |
prd |
Production | Vercel production, Supabase Cloud |
Edge functions run inside the Supabase Docker runtime and can't use doppler run directly. Instead, supabase/.env.local bridges Doppler secrets to the edge runtime:
# Generate supabase/.env.local from Doppler
./scripts/sync-env-local.sh
# Serve edge functions locally
supabase functions serve --env-file supabase/.env.localThis file is gitignored. Run sync-env-local.sh whenever you rotate keys in Doppler.
Seeded automatically by supabase db reset:
| Field | Value |
|---|---|
[email protected] |
|
| Password | testpassword123 |
| UUID | 00000000-0000-0000-0000-000000000001 |
| Profile | Test Taster, casual style |
This user has 20 tastings, 3 scans, and 1 pending queue item seeded for testing.
pnpm dev # Start Next.js dev server (injects Doppler secrets)
pnpm lint # Lint the web app
pnpm --filter vinho-web run typecheck # TypeScript checkpnpm --filter vinho-web run test # Unit tests (no Supabase needed)
pnpm --filter vinho-web run test:integration # Integration tests (requires local Supabase)
pnpm --filter vinho-web run test:ci # All tests for CIpnpm simulate # Upload 10 wine label images and queue them
pnpm simulate:process # Same, plus invoke the process-wine-queue edge functionThe simulation uploads real wine label fixtures from supabase/fixtures/wine-labels/ to local storage, creates scan records, and queues them for processing.
supabase start # Start local Supabase
supabase db reset # Wipe and rebuild from migration + seeds
supabase migration new <name> # Create a new migration
supabase db push # Push migrations to production (or via CI)
supabase gen types typescript --local > apps/vinho-web/lib/database.types.ts # Regenerate typessupabase functions serve --env-file supabase/.env.local # Serve all functions locallyvinho/
apps/
vinho-web/ # Next.js 16 web app
vinho-ios/ # SwiftUI iOS app
vinho-android/ # Jetpack Compose Android app
supabase/
migrations/ # Production schema (single pulled migration + incremental)
migrations-archive/ # Old local-only migrations (preserved for reference)
functions/ # Edge Functions (process-wine-queue, generate-embeddings, etc.)
fixtures/wine-labels/ # 10 real wine label images for simulation
seeds/ # Seed SQL source files
seed.sql # Active seed (test user, 20 wines, tastings, scans)
config.toml # Local Supabase config (ports, buckets, auth)
.env.local # Edge function secrets (gitignored, generated from Doppler)
shared/ # Shared utilities for edge functions
scripts/
sync-env-local.sh # Sync Doppler secrets to supabase/.env.local
simulate-wine-upload.ts # Wine upload simulation (lives in apps/vinho-web/scripts/)
.github/workflows/
ci.yml # PR checks: lint, typecheck, unit + integration tests
deploy.yml # On merge to main: push migrations, deploy edge functions
GitHub Actions runs lint, typecheck, and both unit and integration tests (with local Supabase) on every PR to main.
On merge to main:
- Supabase migrations are pushed via
supabase db push - Edge functions are deployed via
supabase functions deploy - Vercel auto-deploys the web app
| Secret | Description |
|---|---|
SUPABASE_ACCESS_TOKEN |
Supabase CLI auth token |
SUPABASE_PROJECT_ID |
aghiopwrzzvamssgcwpv |
When a user scans a wine label, Vinho runs a multi-step pipeline that identifies the wine, creates database records, and builds a searchable index for future scans.
- Image capture: Client compresses to max 2000px JPEG at 0.8 quality
- Upload and queue: Image goes to Supabase Storage
scansbucket, scan record created, queue item inserted aspending - Visual embedding match (fastest): Jina CLIP generates 768-dim embedding, searches
wine-labelsvector bucket. Threshold: 92% similarity - Text vector match (free): Supabase gte-small generates 384-dim embedding from OCR text, searches
wine_embeddingsvia pgvector. Threshold: 90% similarity - OpenAI Vision extraction: GPT-4o-mini extracts structured data. Below 60% confidence, escalates to GPT-4o
- Record creation: Region, producer, wine, vintage, and grape varietals created with upsert logic
- Embedding storage: Visual embedding stored for future matches, text embedding queued
- Enrichment: Separate queue fills in tasting notes, food pairings, aging potential via GPT-4o-mini
| Match method | Speed | Cost | Threshold |
|---|---|---|---|
| Visual embedding (Jina CLIP) | < 2s | Low | 92% |
| Text vector (gte-small) | 2-4s | Free | 90% |
| OpenAI Vision (GPT-4o-mini/4o) | 8-15s | Highest | 60% escalation |
Private. All rights reserved.