Skip to content
Return to Projects
Case Study

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

Next.js 15TypeScriptPostgreSQLpgvectorClaude AIDocker

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

Edge
Cloudflare Tunnel
Application
Next.js 15 SSR
Subscriptions
Gamification
Intelligence
Claude AI
Flavor Profiler
Data
PostgreSQL
pgvector (semantic search)
Runtime
Docker

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.

  1. 1Move embedding generation to a background queue — synchronous calls during admin product writes briefly blocked the UI.
  2. 2Add a staging container that mirrors prod data shape so AI tuning doesn't happen against the live DB.
  3. 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

TriggerXP AwardCondition
Complete AI quiz+50 XPOn quiz.submit success
First purchase+100 XPorder_count transitions 0 → 1
Any subsequent purchase+30 XPPer order, not per item
Subscription renewal+250 XPOn invoice.paid webhook
Leave a review+30 XPMin 50 chars, once per product
Refer a friend+200 XPOn referee's first completed order
Read a brew guide+40 XPOnce per guide
Complete profile+50 XPName + preferences filled
Share brew photo+40 XPVia tracked UTM link
Rate a product+10 XP1–5 stars, once per product

#Level Tiers

LevelXP RequiredPerks
Bronze0Base XP rewards, welcome pack
Silver500Free brew guide PDF download
Gold1,0005% discount on all orders, priority drop access
Platinum1,80010% discount, rare lot access, private cupping events
Black3,00015% discount, direct sourcing access, farm communications

#Badges

Badge NameIconTriggerXP Gate
First BrewFirst order completed0
Origin Explorer🌍Ordered from 3 different origin countries100
Subscriber🔄Active subscription created0
ReviewerFirst review submitted0
Referrer👥First successful referral0
Cupping Pro🏆Read 3+ brew guides500
Gear Head⚙️Purchased equipment category product0
Roast Master🔥Ordered all 5 roast levels800
Platinum Brewer💎Reached Platinum level1,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

PlanXP MultiplierMonthly Bonus
None1.0×
Starter1.0×+100 XP
Connoisseur2.0× (on purchases)+250 XP
Collective3.0× (on purchases)+500 XP

Next: 07 — Subscription System →

~ End of Document ~