# yaml-language-server: $schema=../references/diagram.schema.json
title: Modern SaaS web stack
subtitle: Next.js on Vercel serves the UI and Cloudflare fronts the API; Neon and Upstash hold the data while Clerk, Stripe and Resend handle auth, payments and email
direction: down
# Product cards keep the managed services compact.
look: card
# One port per edge, so the checkout, webhook and email edges fan out of the API without crossing.
layout: { spacing: compact, merge: false }

legend:
  solid: Synchronous request
  dashed: Webhook
  dotted: Errors and analytics

groups:
  edge: { type: zone, label: Edge }
  ui: { type: zone, label: Frontend }
  app: { type: zone, label: Backend }
  data: { type: zone, label: Data }
  auth: { type: zone, label: Identity }
  saas: { type: zone, label: Third-party services }
  obs: { type: zone, label: Observability }

nodes:
  web: { icon: generic:browser, label: Web, note: Browser }
  mobile: { icon: generic:mobile, label: Mobile app, note: iOS and Android }
  cloudflare: { icon: logos:cloudflare-icon, label: Cloudflare, note: CDN and WAF for the API, in: edge }
  api: { icon: logos:nodejs-icon, label: API service, note: "Node.js, verifies Clerk JWT", in: app }
  nextjs: { icon: logos:nextjs-icon, label: Next.js app, note: Hosted on Vercel, in: ui }
  neon: { icon: logos:neon-icon, label: Neon, note: Serverless Postgres, in: data }
  upstash: { icon: logos:upstash-icon, label: Upstash, note: Redis cache, in: data }
  stripe: { icon: si:stripe, label: Stripe, note: Payments and billing, in: saas }
  resend: { icon: logos:resend-icon, label: Resend, note: Transactional email, in: saas }
  clerk: { icon: logos:clerk-icon, label: Clerk, note: Auth and sessions, in: auth }
  sentry: { icon: logos:sentry-icon, label: Sentry, note: Error tracking, in: obs }
  posthog: { icon: logos:posthog-icon, label: PostHog, note: Analytics and replay, in: obs }

edges:
  # app.example.com is DNS-only to Vercel (Vercel advises against stacking a proxy in front of it).
  - web -> nextjs: app.example.com
  # The browser and the mobile app call the API through Cloudflare; the Next.js server fetches from it directly.
  - web, mobile -> cloudflare: API calls
  - cloudflare -> api: api.example.com
  - nextjs -> api: server fetch
  # Clients sign in against Clerk; the API only verifies the token it sends.
  - web, mobile -> clerk: sign in
  - api -> neon: SQL
  - api -> upstash: REST
  - api -> stripe: checkout
  - api -> resend: send email
  # Written in layout order with the arrow flipped, so the webhook does not loop around.
  - { from: api, to: stripe, arrow: start, line: dashed, label: webhooks via api.example.com }
  - api, nextjs ..> sentry: errors
  - nextjs ..> posthog: events
