Stack
Layout
Where the logic lives
Business rules live inlib/, 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 * 29is 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 awaitsauth(), 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.