A group-trip planner that reconciles every traveller's budget, tastes and hard "no"s into one fair, AI-written itinerary — built in a hackathon sprint with a deliberately boring, zero-build stack. Everyone's voice, one trip.
Every number on this page is measured from the actual codebase. App: voyagent.netlify.app · Live metrics: Voyagent Pulse
One shared API core runs identically as an Express server locally and a Netlify Function in production — no drift between dev and prod. The browser holds zero secrets: the database denies all direct access (row-level security with no policies + revoked grants), and live updates are empty "something changed" broadcast pings that trigger clients to re-fetch through the API.
| Layer | Technology | Why |
|---|---|---|
| Frontend | Preact 10.25 + htm 3.1 via esm.sh CDN — single index.html (~1,500 lines) | Zero toolchain; edit-and-refresh; ~4KB framework |
| API | Shared api-core.js → Express 5 (local) / Netlify Function (prod), esbuild-bundled | Same code both environments |
| Database | Supabase (Postgres) · SQLite (node:sqlite) fallback for key-less local dev | Managed, realtime, free tier |
| AI | OpenAI gpt-4o default · Anthropic claude-opus-4-8 supported — provider-agnostic wrapper | Strict structured outputs on both |
| Weather | Open-Meteo forecast API (AI-supplied coordinates) | Free, no key, drives rain re-tune |
| Hosting | Netlify (app+API) · Vercel (Pulse, primary) + Netlify mirror (backup) | Git-push deploys, multi-platform resilience |
| PWA | Manifest + network-first service worker + canvas-generated icons | Installable, offline-readable plans |
| Dependencies | @supabase/supabase-js, @anthropic-ai/sdk, express — that's all | Tiny attack/maintenance surface |
10 screens (Home hub, Create, Join, Invite, Quiz, Group, Suggestions, Vote, Generating, Itinerary) + 4 overlay sheets (Trip settings, Re-tune, Trip chat, Surprise roulette), all in one Preact file with light/dark Zen theming.
4-step 60-second quizper-day budgets ₹/$/€/£ (log-scale slider)optional age bandstrip-aware avoid presetshard dealbreakersdestination voting (live tally)AI day-by-day itineraryweather-aware re-tunechat that edits the schedulevoice input ×3 fieldssurprise-destination rouletteconsensus gatesstale-plan rebuild bannerpublic share pages (zero-JS)realtime roster/votes
| Method | Path | Purpose |
|---|---|---|
GET | /api/config | Runtime config (AI provider, anon realtime key) |
GET | /api/stats?month=YYYY-MM | Public aggregate analytics (CORS-open, 60s cache, month filter) |
POST | /api/trips | Create trip |
POST | /api/trips/join/:code | Join via invite code |
GET | /api/trips/:id | Full trip state (members, prefs, votes, plan) |
POST | /api/trips/:id/preferences | Submit/update quiz |
POST | /api/trips/:id/settings | Rename / re-date (creator-only) |
POST | /api/trips/:id/suggest | AI destination suggestions ⚡ |
POST | /api/trips/:id/vote | Cast vote (validated against ballot) |
POST | /api/trips/:id/generate | AI itinerary for vote winner ⚡ |
POST | /api/trips/:id/retune | AI single-day rewrite (preview) ⚡ |
POST | /api/trips/:id/retune/apply | Persist re-tune as new version (validated) |
POST | /api/trips/:id/ask | Trip chat — can directly edit the schedule ⚡ |
GET | /api/trips/:id/weather | Rain probability per trip day (Open-Meteo) |
⚡ = AI route, guarded by rate limits. Every mutating route fires a realtime broadcast ping after writing.
| Table | Cols | Holds |
|---|---|---|
trips | 10 | Trip, phase state machine, private invite code + separate public share_code |
members | 5 | Travellers (first-name only, no accounts) |
preferences | 12 | Quiz answers: per-day budget + currency, vibes, city, age band, dealbreakers |
suggestions | 4 | AI destination cards incl. coordinates |
votes | 4 | One vote per member (composite PK) |
itineraries | 11 | Versioned plans — every re-tune/chat-edit is a new version; unique partial index enforces one active |
ai_calls | 5 | Append-only AI usage ledger → rate limits + spend cap |
7 indexes including UNIQUE (trip_id) WHERE active on itineraries and a unique share-code index. RLS enabled with zero policies + all grants revoked from anon: the public key can read and write nothing.
| Feature | Function | Output budget | Fallback when AI is down |
|---|---|---|---|
| Destination suggestions | suggestDestinations | 3.5k tokens | 3 seeded cards with real coordinates |
| Itinerary generation | generateItinerary | 3k + 1.2k/day (≤16k) | Destination-aware template days |
| Day re-tune | retuneDay | 3.5k tokens | Canned indoor/covered day |
| Trip chat + edits | askTrip | 6k tokens | Safe "plan unchanged" reply |
additionalProperties:false, every field required) — the UI never parses free text.| Control | Implementation |
|---|---|
| Database lockdown | RLS deny-all + revoked anon grants — verified HTTP 401 on every table with the public key |
| Zero-data realtime | Empty broadcast pings only; realtime publication contains no tables |
| HTTP headers | CSP (allow-listed origins only), HSTS, nosniff, DENY framing, strict referrer, Permissions-Policy (mic-only) |
| Share pages | Server-rendered, zero JavaScript, CSP default-src 'none', public code never exposes private invite |
| Input handling | Every field length-capped + control-chars stripped server-side; payload shape validation on all writes |
| Prompt security | Injection/junk/unsafe-input battle-tested and scored by an independent AI judge panel |
| Spend control | 20 AI calls/trip/hr · 30/IP/hr · 300/day global hard cap (env-tunable), append-only ledger, friendly 429s |
| Secrets | Keys live only in host env vars; browser receives only the powerless anon key |
A separate static dashboard (Vercel primary, Netlify backup) implementing Pirate Metrics (AARRR): 6 KPI cards (North Star = shared plans delivered), acquisition/activation/referral funnel, activity sparkline, trending destinations, vibes, budget bands, currencies and age mix — with a month/year filter and 60-second live refresh.
Privacy by construction: the dashboard holds no credentials and consumes only /api/stats — aggregates computed server-side (counts, categories, label-only budget bands, small-bucket suppression). It cannot display a name, a trip, or any single person's data.
Deliberate beta constraints: no accounts (trips are capability URLs), AI content is advisory (verify prices/visas), data resets possible.
Next: Google sign-in (unlocks retention metrics), affiliate booking links (revenue), expense splitting, push notifications, vote deadlines, multilingual, error tracking.
Voyagent · Beta · Everyone's voice, one trip. — Repos: voyagent (app) · voyagent-pulse (this site) on GitHub. Full narrative in the knowledge base.