Brewhaus
An AI-driven specialty coffee marketplace for home enthusiasts.
The Problem
Home coffee enthusiasts struggle to find beans matched to their taste. Brewhaus profiles flavor with AI and recommends roasts that fit each palate — turning discovery into an interactive experience.
My Role
Built solo with AI-assisted development — SSR storefront, vector search, AI integration, and Dockerized deployment.
Highlights
- Next.js 15 SSR storefront with gamification and subscription billing
- pgvector semantic search powering AI-driven flavor profiling
- Claude AI integration for personalized recommendations
- Containerized with Docker and served via Cloudflare Tunnels
Stack
Constraints
- Self-hosted on a home server — no cloud bill, but also no auto-scaling.
- Solo build, so the stack had to favour boring, well-documented pieces over novelty.
- AI cost must stay bounded — recommendations had to use embeddings, not a per-request LLM call.
System Architecture
Key Trade-offs
The decisions worth defending — what I chose, what I turned down, and why.
Recommendation engine
Chose
pgvector similarity on cached embeddings
Rejected
LLM call per recommendation
Vector search is millisecond-fast and effectively free once embeddings are computed; LLMs would have added cost and latency per page view.
Public ingress
Chose
Cloudflare Tunnel
Rejected
Public VPS with open ports
No exposed origin IP, free TLS, and DDoS protection inherited from Cloudflare — much smaller attack surface for a self-hosted box.
Rendering model
Chose
Next.js 15 SSR
Rejected
Pure client-rendered SPA
Product pages need to be indexable and shareable; SSR gives fast first paint and clean OG cards without a separate prerender step.
What I'd Do Differently
An honest retrospective — the stuff I'd change with more time, more users, or a second pass.
- 1Move embedding generation to a background queue — synchronous calls during admin product writes briefly blocked the UI.
- 2Add a staging container that mirrors prod data shape so AI tuning doesn't happen against the live DB.
- 3Track recommendation quality with a click-through metric instead of relying on subjective spot-checks.
Technical Deep-Dive
Architecture, specifications, and implementation details.
06 · Gamification System
#Overview
The Brew XP system is a loyalty and engagement mechanism that rewards every meaningful interaction — purchases, reviews, quiz completion, guide reading, and referrals — with experience points that unlock levels and exclusive perks.
#XP Events
| Trigger | XP Award | Condition |
|---|---|---|
| Complete AI quiz | +50 XP | On quiz.submit success |
| First purchase | +100 XP | order_count transitions 0 → 1 |
| Any subsequent purchase | +30 XP | Per order, not per item |
| Subscription renewal | +250 XP | On invoice.paid webhook |
| Leave a review | +30 XP | Min 50 chars, once per product |
| Refer a friend | +200 XP | On referee's first completed order |
| Read a brew guide | +40 XP | Once per guide |
| Complete profile | +50 XP | Name + preferences filled |
| Share brew photo | +40 XP | Via tracked UTM link |
| Rate a product | +10 XP | 1–5 stars, once per product |
#Level Tiers
| Level | XP Required | Perks |
|---|---|---|
| Bronze | 0 | Base XP rewards, welcome pack |
| Silver | 500 | Free brew guide PDF download |
| Gold | 1,000 | 5% discount on all orders, priority drop access |
| Platinum | 1,800 | 10% discount, rare lot access, private cupping events |
| Black | 3,000 | 15% discount, direct sourcing access, farm communications |
#Badges
| Badge Name | Icon | Trigger | XP Gate |
|---|---|---|---|
| First Brew | ☕ | First order completed | 0 |
| Origin Explorer | 🌍 | Ordered from 3 different origin countries | 100 |
| Subscriber | 🔄 | Active subscription created | 0 |
| Reviewer | ⭐ | First review submitted | 0 |
| Referrer | 👥 | First successful referral | 0 |
| Cupping Pro | 🏆 | Read 3+ brew guides | 500 |
| Gear Head | ⚙️ | Purchased equipment category product | 0 |
| Roast Master | 🔥 | Ordered all 5 roast levels | 800 |
| Platinum Brewer | 💎 | Reached Platinum level | 1,800 |
#Level Calculation
export function calculateLevel(xp: number): UserLevel {
if (xp >= 3000) return "BLACK";
if (xp >= 1800) return "PLATINUM";
if (xp >= 1000) return "GOLD";
if (xp >= 500) return "SILVER";
return "BRONZE";
}
#XP Award Flow
Event occurs (e.g. order confirmed)
↓
awardXP(userId, amount, reason) called
↓
Prisma transaction:
1. UPDATE users SET xp = xp + amount, level = newLevel
2. INSERT INTO xp_events (userId, amount, reason)
↓
checkBadges(userId, newXp)
→ Query badges WHERE xp_required <= newXp
→ INSERT new UserBadge records (skip duplicates)
↓
Return { newXp, newLevel, leveledUp }
↓
If leveledUp → send email notification (BullMQ email queue)
#Frontend Integration
The XP state is shared globally via Zustand:
// useXPStore.ts
interface XPStore {
xp: number
level: UserLevel
badges: UserBadge[]
awardXP: (amount: number, reason: string) => void
showToast: (msg: string) => void
}
Every purchase, quiz completion, and interaction triggers an optimistic XP update on the client, with server confirmation via tRPC mutation response.
#Subscription XP Multipliers
| Plan | XP Multiplier | Monthly Bonus |
|---|---|---|
| None | 1.0× | — |
| Starter | 1.0× | +100 XP |
| Connoisseur | 2.0× (on purchases) | +250 XP |
| Collective | 3.0× (on purchases) | +500 XP |