Skip to main content

Stack

Layout

Where the logic lives

Business rules live in lib/, not in components. Two rules explain most of the structure: Prices live in exactly one place. lib/catalogue.ts holds every purchasable item and its price, so a purchase takes an id and looks the amount up server-side. Anything that trusted a browser-supplied amount would let a caller name their own price. Purchase logic is session-free. lib/purchase.ts takes a user id, not a session. A click carries a signed-in user; a crypto checkout settles from a webhook where there is nobody to read. Both must debit and grant identically, and the webhook path must not have to import the auth stack.

Auth

Two NextAuth instances, deliberately: Both share auth.config.ts, so the proxy validates tokens against the same window the handlers issue them with. Sessions are JWT. The cookie always carries the longer 30-day lifetime; a shorter session is enforced by an expiresAt timestamp inside the token, because a JWT cannot expire when a browser closes.

Streams

Two server-sent event endpoints push updates without polling:

The signal engine is a separate process

lib/signal-engine.ts talks to it over HTTP — one fetch per call, no SDK, 5-second timeout. If SIGNAL_ENGINE_BASE_URL is unset, the UI reports “not connected” rather than rendering sample data.
This API places no orders. Nothing reachable through it can move money — the separation is structural, not configuration.

Money

  • Amounts are fixed-precision decimals, never floats.
  • Referral rates are basis points — 5% of $29 must be exact, and 0.05 * 29 is not.
  • Balances are derived from ledger entries, not stored as a counter.
  • Debit and grant happen in one transaction, with write-conflict retry.

Environment safety

lib/env.ts validates the whole environment with Zod at boot and throws a message naming the field. Related settings are checked together — an API key without its webhook secret is a boot failure, not a runtime surprise at the first callback.

Rendering

The root layout awaits auth(), which opts every route into on-demand rendering. Static param lists exist where they would help (generateStaticParams on blog posts), so pages become statically generated if that ever changes.