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.
04 · API Specification
API Layer: tRPC v11 (primary) + Next.js Route Handlers (webhooks, health)
Validation: Zod schemas on every input
Auth: NextAuth v5 JWT sessions
Rate Limiting: 100 req/min per IP via Redis sliding window
#tRPC Router Structure
appRouter
├── products
│ ├── list (public) — paginated product catalog
│ ├── bySlug (public) — single product with reviews
│ ├── recommendations (auth) — AI-matched products for user
│ └── featured (public) — homepage featured products
│
├── quiz
│ ├── getQuestions (auth) — return quiz questions array
│ └── submit (auth) — process answers → taste profile → XP
│
├── cart
│ ├── get (public) — session-based cart
│ ├── addItem (public) — add product to cart
│ ├── removeItem (public) — remove from cart
│ └── clear (public) — empty cart
│
├── orders
│ ├── create (auth) — create order + Stripe payment intent
│ ├── list (auth) — user order history
│ └── byId (auth) — single order detail
│
├── subscriptions
│ ├── getPlans (public) — return plan config
│ ├── create (auth) — create Stripe subscription
│ └── cancel (auth) — cancel at period end
│
├── profile
│ ├── get (auth) — full profile with XP, badges, stats
│ ├── update (auth) — update name, preferences
│ └── xpHistory (auth) — paginated XP event log
│
├── reviews
│ ├── create (auth) — submit review → award XP
│ └── byProduct (public) — all reviews for a product
│
└── guides
├── list (public) — all brew guides
├── bySlug (public) — single guide content
└── markRead (auth) — award XP for reading guide
#tRPC Procedure Reference
##products.list
Input: {
category?: "BEANS" | "EQUIPMENT" | "ACCESSORIES"
featured?: boolean
search?: string // full-text search
limit?: number // default 20, max 50
cursor?: string // for pagination
}
Output: {
items: ProductWithReviews[]
nextCursor?: string
}
##products.recommendations
// Requires authenticated session
Input: {
limit?: number // default 6, max 10
}
Output: Product[] // AI-matched, re-ranked by Claude Sonnet
##quiz.submit
// Requires authenticated session
Input: {
answers: Array<{
questionIndex: number
question: string
answer: string
}>
}
Output: {
profile: TasteProfile // Claude Haiku output
xpAwarded: number // always 50
newXp: number
newLevel: UserLevel
leveledUp: boolean
}
##orders.create
// Requires authenticated session
Input: {
items: Array<{
productId: string
quantity: number
}>
}
Output: {
orderId: string
clientSecret: string // Stripe Payment Intent client secret
total: number
xpWillEarn: number
}
#REST Route Handlers
##GET /api/health
Returns service health status for Docker healthcheck.
{
"status": "healthy",
"timestamp": "2026-05-11T10:00:00.000Z",
"services": {
"database": "ok",
"redis": "ok"
}
}
##POST /api/webhooks/stripe
Receives and verifies Stripe webhook events. Handles:
| Event | Action |
|---|---|
checkout.session.completed | Create order, award XP |
customer.subscription.created | Upsert subscription record |
customer.subscription.updated | Update subscription status |
customer.subscription.deleted | Mark subscription cancelled |
invoice.paid | Award subscription renewal XP |
invoice.payment_failed | Update status to PAST_DUE |
Headers required:
stripe-signature: <webhook-signature>
Verification:
stripe.webhooks.constructEvent(body, sig, STRIPE_WEBHOOK_SECRET)
##POST /api/trpc/[...trpc]
Main tRPC endpoint. Handles all tRPC procedure calls.
##GET /api/auth/[...nextauth]
##POST /api/auth/[...nextauth]
NextAuth.js authentication routes (Google OAuth, magic link).
#Error Handling
All tRPC procedures return structured errors:
{
code: "NOT_FOUND" | "UNAUTHORIZED" | "FORBIDDEN" |
"BAD_REQUEST" | "INTERNAL_SERVER_ERROR" | "TOO_MANY_REQUESTS"
message: string
data?: {
zodError?: ZodFlattenedError // validation errors
}
}
#Rate Limiting
Global: 100 requests / minute / IP
AI routes: 10 requests / minute / user session
Auth: 20 requests / minute / IP
Webhooks: exempt (verified via Stripe signature)
Next: 05 — AI Engine →