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.
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 Type | Purpose | Retention | Legal Basis |
|---|---|---|---|
| Email address | Auth, notifications | Account lifetime | Contract |
| Name | Personalisation | Account lifetime | Contract |
| Taste profile | AI recommendations | 2 years | Consent |
| Order history | Fulfilment, accounting | 7 years (tax) | Legal obligation |
| XP events | Gamification | Account lifetime | Legitimate int. |
| IP address | Rate limiting | 24 hours | Legitimate 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
.envvalues filled with real secrets -
NEXTAUTH_SECRETgenerated withopenssl 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