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.

07 · Subscription System

#Plans

PlanPriceWeightXP BonusKey Feature
StarterRON 89/mo250g+100 XP/mo1 curated origin per month
ConnoisseurRON 159/mo500g+250 XP/mo2 AI-matched origins + 10% off
CollectiveRON 279/mo1kg+500 XP/mo4 rare origins + 15% off

#Stripe Integration

##Price IDs

Create these recurring prices in the Stripe Dashboard:

STRIPE_PRICE_STARTER      = price_xxx  (RON 89, monthly)
STRIPE_PRICE_CONNOISSEUR  = price_xxx  (RON 159, monthly)
STRIPE_PRICE_COLLECTIVE   = price_xxx  (RON 279, monthly)

##Subscription Creation Flow

1. User clicks "Subscribe" on plan
2. Frontend calls subscriptions.create tRPC procedure
3. Server:
   a. Create Stripe Customer (or retrieve existing)
   b. Create Stripe Checkout Session in subscription mode
   c. Return session URL
4. Redirect to Stripe Checkout
5. User completes payment on Stripe
6. Stripe fires checkout.session.completed webhook
7. Server creates subscription record in DB
8. BullMQ dispatches welcome email
9. XP awarded (+250 for Connoisseur, etc.)

##Webhook Events Handled

checkout.session.completed     → Create subscription in DB
customer.subscription.created  → Upsert subscription record
customer.subscription.updated  → Update plan/status
customer.subscription.deleted  → Mark as cancelled
invoice.paid                   → Award monthly XP bonus, queue delivery
invoice.payment_failed         → Update to PAST_DUE, send email

#Monthly Delivery Process

invoice.paid webhook received
       ↓
BullMQ: dispatch "subscription-delivery" job
       ↓
Worker:
  1. Identify user's subscription plan
  2. Query AI recommendations (2/4 products based on plan)
  3. Create internal fulfilment order record
  4. Send "Your box is being prepared" email
  5. Award monthly XP bonus
  6. Update subscription delivery log
       ↓
Manual fulfilment team notified via dashboard

#Cancellation

// Cancels at period end — user keeps access until renewal date
await stripe.subscriptions.update(stripeSubscriptionId, {
  cancel_at_period_end: true,
});

// DB update
await db.subscription.update({
  where: { stripeSubscriptionId },
  data:  { cancelAtPeriodEnd: true },
});

#Customer Portal

Stripe's Customer Portal handles:

  • Plan upgrades / downgrades
  • Payment method updates
  • Invoice history
  • Cancellation
const session = await stripe.billingPortal.sessions.create({
  customer:   stripeCustomerId,
  return_url: `${process.env.NEXT_PUBLIC_APP_URL}/profile`,
});
redirect(session.url);

Next: 08 — Security & Compliance →

~ End of Document ~