Stack
Directory tree
app/ (inside src/) is the route surface. src/ modules outside app/ are the
business logic that pages and route handlers call. The app/[locale]/ segment is
where i18n routing happens; the route groups (marketing), (auth), (app)
exist for layout grouping and auth gating, not for URL shape.
Load-bearing decisions
Provider facade pattern
Every module that touches an external service exports a single object (the facade) backed by interchangeable providers. The facade picks an implementation at boot fromsiteConfig.<module>.provider. Consumers see only
the facade.
src/ai/, src/mail/, src/newsletter/, src/customer-service/,
src/affiliate/. Adding Postmark for mail is one new file under
src/mail/provider/postmark.ts and one switch arm — no consumer code changes.
Server-only enforcement
Files that touch the DB or read secretsimport 'server-only' at the top. Next.js
fails the build if such a module is imported into a client component. Already
protected: src/db/index.ts, src/lib/auth.ts, src/lib/server.ts,
src/lib/safe-action.ts, every src/payment/provider/*, src/mail/index.ts,
src/credits/index.ts, src/credits/server.ts. Add the line whenever you create
a new server-only module — it’s a half-second of typing that prevents accidental
key leaks.
Three-tier server actions
All mutating actions go throughnext-safe-action with one of three clients
defined in src/lib/safe-action.ts:
actionClient— public, no auth required.userActionClient— gated; providesctx.userfrom the session.adminActionClient— gated; requiresuser.role === 'admin'.
if (!user) throw boilerplate scattered
across the codebase.
Idempotent webhooks
Payment webhooks retry on any non-2xx response. If you double-grant credits on a retry, you’re paying twice for one payment. The scaffold solves this with two guarantees insrc/payment/handlers/:
payment.invoiceIdhas a unique index — duplicate inserts fail the transaction.- Every handler does a
payment.sessionIdlookup before insert and exits early on hit.
Split DB schemas
src/db/ is split by concern: auth.schema.ts (Better Auth), app.schema.ts
(payment + credits + transactions), affiliate.schema.ts, ai.schema.ts.
schema.ts re-exports them all for Drizzle Kit and Better
Auth’s adapter. IDs are text (nanoid prefix or snowflake) — never serial,
because text ids let you migrate between providers without breaking foreign keys
and don’t leak user-count via enumeration. All foreign keys cascade on user
delete (GDPR-friendly out of the box).
Bilingual MDX
Every doc lives in two parallel files:path/to/doc.mdx and path/to/doc.zh.mdx.
Fumadocs is configured to look up the locale-suffixed file first, falling back to
the default. The same pattern works for blog posts and changelog entries. Both
locales must exist for a doc to be considered shipped.
Locale routing — as-needed prefix
src/i18n/routing.ts configures next-intl to keep English at the root (/about)
and prefix Chinese (/zh/about). Use Link / useRouter from
@/i18n/navigation everywhere — they preserve the locale prefix automatically.
Importing from next/link or next/navigation directly will silently lose the
locale on client navigations.
Cost in micro-cents
AI inference and credit math both deal in fractions of a cent. Storing money as floats invites rounding bugs; storing as integer cents loses sub-cent precision (a million-token Claude call costs $0.000003 / token). The scaffold stores everything as integer micro-cents (1/1,000,000 of a USD) and converts to display units at the edge. Seesrc/ai/pricing.ts for the math.
Edge runtime — only for OG image
src/app/opengraph-image.tsx runs on the Edge runtime because it’s the only path
where cold-start latency directly hurts user-visible perf (social-card scrapers
time out fast). Everything else — including all API routes that touch Postgres —
uses the Node runtime, because the pg driver and many auth dependencies
need full Node APIs. Don’t move routes to Edge unless you know what you’re
trading away.
Data flow — checkout
A canonical flow that exercises most of the architecture:See also
- Configuration —
siteConfigknobs. - Customization — common recipes.
- Vercel deployment — ship the architecture.
- Env reference — every key that selects a provider.