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.

08 · Security & Compliance

#Security Architecture

##Network Security

THREAT: Direct server attack / port scanning
MITIGATION: Zero exposed host ports.
  - All containers on internal Docker bridge network
  - Only cloudflared opens an OUTBOUND connection to Cloudflare
  - No inbound firewall rules needed — nothing listens on host

THREAT: DDoS attack
MITIGATION: Cloudflare edge absorbs all traffic before it reaches server.
  - Cloudflare Magic Transit for L3/L4
  - Cloudflare DDoS protection on all zones (automatic)

THREAT: TLS downgrade / MITM
MITIGATION: Cloudflare enforces TLS 1.3 minimum on all connections.
  - HSTS preload header on all responses
  - Internal Docker traffic stays on private bridge network

##Application Security

THREAT: SQL injection
MITIGATION: All queries via Prisma ORM with parameterised inputs.
  No raw SQL except pgvector operations — those use $executeRaw with 
  typed vector literals, not user input.

THREAT: Input validation attacks
MITIGATION: Zod schemas validate every tRPC input before any logic runs.
  Type-safe throughout — TypeScript strict mode enforced.

THREAT: Authentication bypass
MITIGATION: NextAuth v5 with PKCE for OAuth flows.
  JWT tokens signed with NEXTAUTH_SECRET (256-bit random key).
  All protected tRPC procedures check session before executing.

THREAT: Rate limit abuse / API scraping
MITIGATION: Redis sliding window rate limiter.
  100 req/min globally, 10 req/min on AI endpoints per user.
  Applied at Next.js middleware layer before any DB/AI calls.

THREAT: CSRF attacks
MITIGATION: tRPC uses POST with Content-Type: application/json.
  SameSite=Strict on session cookies.
  NextAuth handles CSRF tokens on auth routes.

#PCI DSS Compliance

Brewhaus is PCI DSS compliant by not handling card data:

ALL payment data is processed exclusively by Stripe.

What Brewhaus stores:
  ✓ Stripe Payment Intent ID (opaque reference)
  ✓ Stripe Customer ID (opaque reference)
  ✓ Stripe Subscription ID (opaque reference)
  ✓ Order total (after completion)

What Brewhaus NEVER stores:
  ✗ Card numbers
  ✗ CVV codes
  ✗ Expiry dates
  ✗ Billing addresses
  ✗ Bank account details

Client-side: Stripe.js + Payment Element handles card collection.
The card data never touches Brewhaus servers.

#GDPR Compliance

##Data Collected

Data TypePurposeRetentionLegal Basis
Email addressAuth, notificationsAccount lifetimeContract
NamePersonalisationAccount lifetimeContract
Taste profileAI recommendations2 yearsConsent
Order historyFulfilment, accounting7 years (tax)Legal obligation
XP eventsGamificationAccount lifetimeLegitimate int.
IP addressRate limiting24 hoursLegitimate int.

##User Rights Implementation

// Right to access — export all user data
GET /api/user/export
→ Returns JSON of all user data including orders, profile, XP

// Right to deletion — delete account and cascade
DELETE /api/user/account
→ Prisma cascades: user → orders, reviews, badges, profile
→ Stripe: cancel subscription, delete customer
→ Redis: clear session data
→ Confirmation email sent

// Right to rectification — update profile
PUT /api/user/profile
→ Update name, email, preferences

##Cookie Consent

Essential cookies (no consent required):
  - __Secure-next-auth.session-token (auth)
  - brewhaus-cart (cart state, localStorage)

Analytics cookies (require consent):
  - _cf_bm (Cloudflare bot management)
  
No advertising cookies. No third-party tracking.
Brewhaus is ad-free — no data sold or shared with advertisers.

#Docker Security

# Run as non-root user
RUN addgroup --system --gid 1001 brewhaus \
 && adduser  --system --uid 1001 brewhaus

USER brewhaus

# Read-only filesystem where possible
# Postgres and Redis bind to internal network only
# Secrets via environment variables, never baked into images

#Secrets Management

RULE: No secrets in git.
      No secrets in Docker images.
      No secrets in logs.

IMPLEMENTATION:
  - All secrets in .env file (gitignored)
  - Docker Compose reads via env_file or environment
  - NEXTAUTH_SECRET: openssl rand -base64 32
  - DB_PASS: openssl rand -base64 24
  - CF_TUNNEL_TOKEN: from Cloudflare dashboard only

ROTATION POLICY:
  - NEXTAUTH_SECRET: rotate every 90 days
  - DB_PASS: rotate after any suspected breach
  - CF_TUNNEL_TOKEN: rotatable without downtime (delete + recreate tunnel)
  - Stripe keys: rotate via Stripe dashboard, update .env, restart app container

#Security Headers

Applied via next.config.ts on all routes:

X-Frame-Options:           DENY
X-Content-Type-Options:    nosniff
Referrer-Policy:           strict-origin-when-cross-origin
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Security-Policy:   (see next.config.ts for full policy)

#Security Checklist Before Go-Live

  • All .env values filled with real secrets
  • NEXTAUTH_SECRET generated with openssl rand -base64 32
  • Stripe webhook secret verified with stripe listen --forward-to
  • Cloudflare Tunnel token from dashboard (not example value)
  • PostgreSQL password is strong (32+ random chars)
  • HSTS preload submitted to hstspreload.org
  • Stripe webhook endpoint verified and active
  • GDPR cookie banner implemented on frontend
  • Data export endpoint tested
  • Rate limiting verified with load test

Next: 09 — Frontend Design System →

~ End of Document ~