Problem
A guided wine tasting is a great plan with friends, but it needs a sommelier, a venue and a booking. Doing it at home usually means opening bottles with no structure: nobody knows what to look for, and the evening never becomes the shared experience it could be.



Solution
3Wines is a platform for gamified wine tastings. A physical pack brings the wines home, and the tasting runs as a live game on the TV, with players joining from their own phones.
My role
The project started as a team build at a product hackathon, where it won the project competition. After that, I took it over on my own and rebuilt it as a 2.0 to fix scalability issues: I reworked the landing, host app, player app and TV mode, moved the database to my own project and audited it, and created a completely new branding, closer to the wine sector. I owned product design and the 2.0 build, with an AI agent writing the code under my direction and review.
What I did
- Game design: session flow, phases, scoring and the sommelier script. Tools: Markdown specs, Claude Code, ElevenLabs
- UX/UI for three screens in sync: TV room, player phone and host controls. New brand identity. Tools: Lovable, Claude Code, ShadCN (Tailwind CSS + Radix)
- Real-time multiplayer with the host as the single source of truth. Tools: Claude Code, TanStack Start, Supabase
- Checkout and order fulfilment. Tools: Stripe
- Repository, database and deployments. Tools: Claude Code + MCP/CLI (Supabase, GitHub, Vercel)
How it works
-
1. Landing and packs
-
2. Cart and Checkout
-
3. Live game

New wines, same game
Packs rotate every 3 months. Each rotation is a new reason to buy again, while the game stays the same: same rounds, phases and scoring, with new wines in the glass. Keeping that rotation cheap to run is what shaped the game engine.
- 1Pick 3 new wines
- 2Import the wine data (CSV)
- 3Answer options built from the data
- 4New sommelier clips, routes updated
- 5Ship the new packs
Validated at real scope
The hackathon win validated the concept. The rebuild was validated by playing it: full sessions gameplay, real wine data and a TV. Demo mode allowed testing a full session without a pack, which made every iteration quick to check.
Some good product choices
On /play, the host answers the same quiz as everyone else. On /room, a compact footer bar for gameplay management; left: label contains the player URL reminder; center: start, next phase, reveal all-in-one component; right: flat, icon-based volume and cast controls. The cast control has a tutorial tooltip on top. The host's client is the single source of truth: it owns RoomState and broadcasts it. Players and the TV only render what they receive and send answers back. No player can advance the game.

Decouple the quiz engine from the wines. Session structure (3 wines × sight / nose / palate / bet) stays in code. Questions, answers and wine cards come from the database per pack, through a QuizSource interface. Demo mode keeps a local source with the same contract. Adding a new pack must not require a deploy.
That keeps every pack rotation a content job, not an engineering one: update the mp3 clips and their routes, and the game is ready.
The game is a shell
The hackathon version had the content as directly wired mp4 vids. To scale, the session structure had to stay fixed while the wines change: same rounds, phases and timers, with only the content of each wine swapped in the shape of light-weight, copy + AI-generated mp3 clips.
// Fixed in code: 3 wines × 4 phases export const WINE_COUNT = 3; // sight · nose · palate · bet export const FASES: readonly Fase[] = ["vista", "nariz", "boca", "bet"]; // reveal → next phase, or the wine's podium const faseIdx = FASES.indexOf(state.fase); const nextFase = FASES[faseIdx + 1]; if (nextFase) return { ...state, fase: nextFase, step: "quiz", }; return { ...state, stage: "wine_podium" };
// Swapped per pack: database or demo export type QuizSource = { source: "bd" | "demo"; // prompt + options, never the answer questionFor(wineIndex: number, fase: Fase): { prompt: string; options: string[] }; closeQuiz(/* … */): Promise<CloseResult>; };
- +10 pts
Sight
- +10 pts
Nose
- +10 pts
Palate
- +20 pts
Grape type
- +30 pts
Classification
- +15 pts
Price range

Replace the scoring model to match the landing's "How scoring works". Sensory phases (sight, nose, palate) = 10 pts per correct answer. The bet phase rotates by wine: grape type 20, classification 30, price range 15. Remove the speed bonus: every correct answer is worth the same, whoever sends it first.
How it was built
Backend
- Jul 08Schema moved to my own Supabase project
- Jul 08Wine prices as decimal €, unused cost column dropped
- Aug 05Security and performance audit fixes
- Aug 05Storage policies per bucket, unused tables dropped
Game
- Jun 25TV view with sommelier clips per phase
- Aug 062.0: room, player and TV overhaul
- Aug 06Fixed points per phase
- Aug 06Audio unlock for the sommelier voice
- Aug 07Final podium redesign with confetti
Brand and commerce
- Aug 11Cart and checkout polish
- Sep 14Rebrand to 3Wines, v2 landing
The quiz engine, edge functions and Stripe flow came from the hackathon build and stayed. The 2.0 work went into what players see and touch, and into a security pass the MVP never had.
Claude Code was connected to the stack through MCP and CLI (Supabase, GitHub, Vercel). A learnings.md log recorded every non-obvious fix, so the same mistake was never debugged twice.
From hackathon MVP to 2.0
3Wines started as a team build at a hackathon, planned from a PRD and shipped spec by spec. After it won, I forked it and carried it on alone. I moved the backend to my own Supabase project, audited it, and rebuilt the three screens, the scoring and the landing into a 2.0 under a new brand. Claude Code wrote the code; I set the direction, made the product calls, reviewed every change and hand-edited the CSS.
- Trigger
- Supabase's advisor audit flagged the inherited schema on my own project.
- Fixes
-
- Views run with the caller's permissions, not the owner's.
- Any signed-in user could list admin IDs. Now only admins can.
- Function search paths pinned, missing foreign-key indexes added, overlapping admin policies split per action.
- Product decision
- The public monthly ranking is served through one read-only function, so player tables stay closed.
/* Answer reveal on the player's phone */ @keyframes shake-x { 0%, 100% { transform: translateX(0); } 15% { transform: translateX(-8px); } 30% { transform: translateX(7px); } /* … settles down to 1px */ } ._3w-answer-shake { animation: shake-x 0.55s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; } ._3w-answer-rubberband { animation: rubberband-x 0.9s ease-out both; transform-origin: center; }
Takeaways
The game is only one step
Checkout, shipping and activation decide whether the evening ever starts. Designing the whole loop matters as much as designing the game.
Magic in a demo, a bill at scale
A live AI sommelier is impressive on stage and expensive at a thousand tables. Pre-rendered clips per phase keep the experience close at a fraction of the running cost.
One source of truth
With three screens in sync, letting a single host own the state removed a whole class of bugs before they appeared. A clear rule for coding is also the simplest one to build.