System design

How this site
actually works.

Not the brief it was commissioned from — the system that exists. The request path, the data model, the twenty phases it was built in, and the trade-offs behind the parts that are deliberately not here.

The request path.

Five layers, and one rule that shapes all of them: a Vercel function keeps nothing in memory between requests, so every piece of state lives in Postgres or in the URL.

Request pathA request enters through the Vercel CDN and the proxy auth gate, is rendered by server components or handled by a route handler, reaches Postgres through Prisma behind a fallback-on-failure read wrapper, and fans out to Resend and R2 for mail and object storage.ClientBrowserReact 19 · islands onlyEdgeVercel CDNstatic + ISR cacheproxy.tsauth gate, no adapterApplicationServer Componentsrender + data fetchRoute Handlers/api/*after()post-response writesDataPrisma 7Neon driver adapterreadOrFallbackdegrade, never 500Stores & third partiesNeon Postgrespooled · single sourceResendtransactional mailCloudflare R2presigned PUT
Read top to bottom. The proxy runs an adapter-less second Auth.js instance so a route can be gated without loading Prisma or bcrypt on every request.

The data model.

Nine of the principal tables. The through-line is referralRef: one column on three unrelated conversion paths is what turns short links into attribution without touching any of them.

Core data modelNine of the principal tables. A user places bookings; referral links attribute contact messages, bookings and newsletter signups through a shared referralRef; page views aggregate into heatmap cells rather than being stored as raw coordinates.UseridemailrolepasswordHashProjectslugtitleaccenttags[]BlogPostslugpublishedcontentBookingreferencestatusamountMinorReferralLinkcodeclicksuniquesPageViewviewIddayvisitorHashContactMessageemailreferralRefMediaAssetsha256urlbytesHeatmapCellgridXgridYcountplacesattributed byreferralRefviewed asaggregates to
PageView carries a redundant day column on purpose — Prisma has no date truncation in groupBy, so materialising the bucket key keeps a daily chart proportional to the number of days rather than to traffic.

Built in twenty phases.

Each one shipped and verified before the next started, and all twenty are deployed and reachable today. Five are marked as waiting on something outside the code — four on a credential, one on a DNS move. Those phases are live; one feature inside each stays switched off, degrading quietly rather than erroring, until it lands.

  1. P1FoundationTS, Tailwind, CIlive
  2. P2AuthAuth.js v5, RBACneeds OAuth apps
  3. P3AdminPrisma CRUD, gatedlive
  4. P4Contentranked search, sitemaplive
  5. P5IntegrationsGitHub, LeetCodeneeds Cal.com
  6. P6CommsSSE chat, web pushneeds a domain move
  7. P7PaymentsRazorpay, invoicesneeds Razorpay keys
  8. P8Growthshort links, QRlive
  9. P9StorageR2, content-addressedneeds R2 keys
  10. P10Analyticsfirst-party, cookielesslive
  11. P11Realtimepresence, live dashboardlive
  12. P12Deliveryvendored fonts, split CSSlive
  13. P13SecurityCSP, CSRF, HSTSlive
  14. P14DevOpshealth checks, metricslive
  15. P15QA41 E2E, axelive
  16. P16AIhybrid retrieval, citedlive
  17. P17DevEx UICmd+K, terminallive
  18. P18DistSysoutbox, leases, Raftlive
  19. P19Low-levelWASM, threads, WebGPUlive
  20. P20Data engELT, DuckDB, lineagelive

Decisions worth defending.

The interesting part of a system is usually what it does not do. Open any one of these to read the argument.

SSE and a DB poll, not WebSockets

Vercel functions cannot hold a socket open, and module-scope memory is not shared between invocations. Chat streams over SSE; typing indicators are deadline columns in Postgres rather than in-process state.

Redis is optional, not infrastructure

The brief made it load-bearing. The limiter prefers Upstash when a key is configured and falls back to an in-memory token bucket per instance when one is not — a weaker ceiling, accepted deliberately, because a limiter that returns 429 while its own dependency is down has converted someone else's outage into ours. Anything that must actually hold, like payment capture, is guarded by auth and idempotency keys instead.

The answer is extractive by default

Retrieval is hybrid: dense vectors and Postgres full-text search fail in different ways, so both run on every question and the results fuse by reciprocal rank. The answer is then assembled from sentences that exist verbatim on the site, each linking to where it came from. Generation is opt-in and runs in the visitor's own browser — on a page about a real person's real work, a fluent invented claim is not a smaller failure than an awkward true one.

Reads degrade instead of failing

Every public read goes through readOrFallback. A database blip renders an empty section rather than a 500, which is also why the build survives having no database at all — the CI job asserts exactly that.

Heatmaps are aggregates, never raw points

An exact click trail is a behavioural fingerprint and grows without bound. Counts live per grid cell, created lazily, so the table tracks where people click rather than how much traffic there was.

Charts are hand-written SVG

Including the two on this page. They are server-rendered from data already in hand, so no charting library ships and nothing here hydrates.

Money is integer minor units

End to end, never floats. Bookings only reach CONFIRMED through a signature-verified webhook, never from a client callback.

Spec versus reality.

The original brief asked for a larger stack than this. Each row is what it specified, what runs instead, and why — including the four that have shifted since this table was written, two of them from not built to built.

SpecifiedBuiltWhy
RedisUpstash-capable, memory todayno key set; it degrades rather than failing closed
WebSocketsSSE + DB pollserverless cannot host a socket
pgvector / RAGbuilt in P16 — hybrid, citedthe embedding is in-process TF-IDF; every hosted one is metered
Dockera Dockerfile, not the deploy pathit proves nothing here depends on Vercel
Better AuthAuth.js v5the spec allowed the swap
StripeRazorpayIndia-side rails
Playwright / Lighthouse CI41 E2E + axe + Lighthousebuilt in P15; green in CI since run #38

Consensus, watchable.

Raft leader election, running live. The rules are a pure state machine in lib/distsys/raft.ts with nineteen tests asserting what the protocol actually guarantees — a term only increases, two leaders cannot exist in one term, a leader that loses quorum steps down. This component only draws it and drives the clock.

Kill the leader and watch a new one get elected. Kill a third node and watch it correctly elect nobody: two of five cannot form a majority, and stopping is the right answer rather than splitting the cluster in half.

Raft cluster0t01t02t03t04t0

5/5 up · majority needs 3 · electing…

  1. t0Cluster of 5 started. All followers, term 0.

The code is public if you would rather read it than take my word for any of this.