Skip to content
hygge

Architecture

Tech stack, data flow diagrams, security model, and deployment shape of Hygge.

Last updated 2018-10-20

The system is intentionally small — a few stateless route handlers, a Postgres database, and three external APIs. There's no queue, no Redis, no microservices.

Stack#

LayerChoiceWhy
FrontendNext.js 16 (App Router, RSC)Server components keep auth + DB out of the client bundle
DatabaseSupabase (Postgres + Auth + RLS)Managed Postgres with built-in auth and Row-Level Security
PaymentsWhop API + webhooksWe don't custody funds; per-merchant API keys
StorefrontShopify Admin API + script tagStandard OAuth scope set, no theme edits
HostingVercelEdge-friendly, our middleware runs there
EmailResendTransactional only — receipts, payment failures, GDPR notifications
MonitoringSentry + PostHog + Vercel AnalyticsError reporting, product events, perf

The whole app runs in a single Next.js project. Two long-lived clients show up everywhere:

  • Anon Supabase client — created from the user's session cookie, used in dashboard pages and OAuth flows. Honors RLS.
  • Service-role Supabase client — used in webhooks and admin endpoints. Bypasses RLS by design.

Data flow#

Buyer payment path (the hot path)#

Shopify storefront payhygge.com Whop ───────────────── ───────────── ──── script_tag onload │ intercepts checkout click ▼ POST /api/checkout/create-from-cart ──┐ ◄── { checkoutId } │ │ │ Postgres INSERT checkouts ▼ │ POST /api/checkout/get-plan ───────────┼─── checkTransactionLimit ──── merchants table │ 402 if limit reached or no sub │ ◄── { planId } └─── POST /api/v2/plans ─────► Whop creates plan │ ▼ Redirect buyer to Hygge drawer │ │ buyer pays via Whop ▼ POST /api/webhooks/whop ◄───── payment.succeeded │ HMAC verify │ INSERT transactions │ POST /admin/api/orders ──► Shopify ▼ sendSaleNotificationEmail (Resend)

SaaS subscription path#

Same webhook endpoint (/api/webhooks/whop) handles two distinct event sources, disambiguated by data.product_id:

  • If product_id matches one of our Hygge SaaS tier products → SaaS branch (membership + payment events update merchants.subscription_*)
  • Otherwise → buyer-payment branch (above)

We can do this safely because the secret used to verify a SaaS event (WHOP_WEBHOOK_SECRET_HYGGE) is different from the per-merchant buyer secret stored in merchants.whop_webhook_secret. The handler tries each in order.

Security model#

HMAC verification on every webhook. Both Shopify and Whop sign their payloads. We re-compute the HMAC against the raw body using a timing-safe comparison (crypto.timingSafeEqual). Failed verifications return 401 and are never persisted.

Per-merchant Whop secrets. Each merchant's whop_webhook_secret is stored encrypted at rest. Even if a single merchant's secret leaks, no other merchant's payments are forgeable.

Server-side price verification. Cart totals are re-fetched from Shopify's Admin API before we accept them. If a buyer manipulates the cart payload (DevTools, intercepting proxy), the server-computed total wins.

Row-Level Security on Supabase. Merchants can only read their own row, their own checkouts, their own transactions. Service-role access (used by webhooks + admin endpoints only) bypasses RLS — these endpoints are HMAC-verified or session-protected.

No PII in analytics. PostHog identifies merchants by Supabase user.id (UUID) only — no email, no IP, no card data. Sentry has sendDefaultPii: false and a beforeSend that strips cookie and authorization headers as a defense-in-depth.

GDPR + Shopify App Store. Three mandatory webhook handlers (customers/data_request, customers/redact, shop/redact) plus an audit log — see Webhooks for details.

Deployment#

Single Vercel project. The Next.js middleware runs on Vercel's edge runtime; everything else is Node.js serverless functions or RSC server-rendered pages.

  • Cold starts: rare in practice (Vercel keeps frequently-hit functions warm), but the buyer payment path doesn't depend on the cold function — by the time the webhook fires, the buyer is already at the success page on Whop's side.
  • Deploy cadence: every push to main auto-deploys. We use the standard Vercel preview deploys for PRs.
  • Rollback: instant, via Vercel UI. Database migrations are designed to be backward compatible (additive columns, no destructive renames in normal releases).

What this architecture is NOT#

  • Multi-region. Single primary in Vercel/Supabase US East. Adequate for current scale; multi-region is a V2 problem when latency budgets get tighter.
  • Queue-based. Webhooks process synchronously. Whop and Shopify both retry on non-2xx, which is our retry mechanism.
  • Microservice'd. One Next.js project. We'd split out shop/redact and other long-running jobs into a worker if data volumes warrant it — not yet.
Spotted a typo or wrong fact? hello@payhygge.com — we ship doc fixes the same day.