> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vibestrap.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Architecture

> How Vibestrap is wired — stack, layout, and the load-bearing decisions.

Vibestrap is a Next.js 15 app with strict TypeScript, Drizzle on Postgres, Better
Auth for sessions, next-intl for i18n, and Tailwind v4 for styling. The design
target is "ship a paid SaaS in a weekend" — every architectural choice is in
service of that, and there's a short list of opinionated decisions that hold the
whole thing together. This page is the short list.

## Stack

| Layer     | Tech                                                     | Why                                              |
| --------- | -------------------------------------------------------- | ------------------------------------------------ |
| Framework | Next.js 15 (App Router, RSC, Turbopack)                  | Server-first rendering, mature ecosystem.        |
| Runtime   | React 19                                                 | Async server components, `use()` hook.           |
| Language  | TypeScript 5 (strict)                                    | Catches the wrong shape before it ships.         |
| Styling   | Tailwind v4 + shadcn/ui                                  | OKLCH colors, CSS-first config in `globals.css`. |
| ORM       | Drizzle                                                  | Typed schema, no codegen, raw-SQL escape hatch.  |
| Database  | Postgres only                                            | One source of truth, transactions, JSONB.        |
| Auth      | Better Auth + Drizzle adapter                            | Server-side sessions, OAuth, email verification. |
| i18n      | next-intl                                                | Bilingual EN/ZH with `as-needed` URL prefix.     |
| Docs      | Fumadocs MDX                                             | Bilingual `*.zh.mdx` with locale-aware lookup.   |
| Payments  | Stripe / Creem / NOWPayments                             | Behind a single facade.                          |
| AI        | Mock / OpenRouter / OpenAI / Anthropic / Replicate / fal | Same facade pattern; mock runs offline.          |
| Mail      | Resend + React Email                                     | Templated, type-safe, dev-rendered.              |

## Directory tree

```
vibestrap/
├── content/                 # MDX content (bilingual)
│   ├── docs/                # *.mdx + *.zh.mdx parallel pairs
│   ├── blog/
│   └── changelog/
├── messages/                # next-intl message bundles
│   ├── en.json
│   └── zh.json
├── public/                  # static assets, OG image, logo
├── drizzle/                 # generated SQL migrations (committed)
├── src/
│   ├── app/                 # Next.js App Router
│   │   ├── [locale]/        # locale-prefixed routes
│   │   │   ├── (marketing)/ # public pages — home, pricing, blog, docs
│   │   │   ├── (auth)/      # login / register / forgot-password
│   │   │   └── (app)/       # authed — settings, admin, dashboard
│   │   ├── api/             # route handlers (auth, webhooks, ping)
│   │   ├── layout.tsx       # root html shell, fonts, theme
│   │   └── globals.css      # Tailwind v4 @theme + tokens
│   ├── components/          # blocks, layout, ui (shadcn), features
│   ├── config/site.ts       # central config, < 250 lines
│   ├── db/                  # split schemas: auth / app / affiliate / ai / license
│   ├── payment/             # facade + 4 providers + webhook handlers
│   ├── ai/                  # facade + 5 providers + cost / pricing
│   ├── credits/             # 4-type ledger (server-only)
│   ├── mail/                # facade + Resend + React Email templates
│   ├── newsletter/          # facade + Resend / Beehiiv
│   ├── customer-service/    # widget-loader (Crisp / Tawk / Intercom / Chatwoot)
│   ├── affiliate/           # script loader + internal tracking
│   ├── analytics/           # GA / PostHog / Plausible / Umami fan-out
│   ├── i18n/                # routing.ts + request.ts + navigation helpers
│   ├── lib/                 # auth, safe-action, server, utils
│   ├── env.ts               # zod-validated env vars
│   └── middleware.ts        # i18n routing + auth gating
├── wrangler.toml.example    # Cloudflare Workers config template
└── package.json
```

`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 from `siteConfig.<module>.provider`. Consumers see only
the facade.

```ts theme={null}
// src/payment/index.ts
const providers: Record<PaymentProviderName, PaymentProvider> = {
  stripe: stripeProvider,
  creem: creemProvider,
  nowpayments: nowpaymentsProvider,
};

export function getPaymentManager(name?: PaymentProviderName): PaymentProvider {
  const requested = name ?? siteConfig.payment.defaultProvider;
  if (!siteConfig.payment.enabled.includes(requested)) {
    throw new Error(`Payment provider "${requested}" is not enabled.`);
  }
  return providers[requested];
}
```

Same pattern in `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 secrets `import '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 through `next-safe-action` with one of three clients
defined in `src/lib/safe-action.ts`:

* `actionClient` — public, no auth required.
* `userActionClient` — gated; provides `ctx.user` from the session.
* `adminActionClient` — gated; requires `user.role === 'admin'`.

Each action declares an input schema with Zod. Outputs are typed end-to-end. The
gating is centralized — no per-action `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 in `src/payment/handlers/`:

1. `payment.invoiceId` has a unique index — duplicate inserts fail the
   transaction.
2. Every handler does a `payment.sessionId` lookup before insert and exits early
   on hit.

The credit grant happens in the **same transaction** as the payment row insert.
Either both land or neither does. When you add a new event type (subscription
renewals, refunds), copy this shape — never insert credits in a separate call.

### 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. See `src/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:

```
User clicks "Get Vibestrap $49"
  → /register?plan=vibestrap-promo
  → after signup → /settings/billing
  → createCheckoutAction({plan})    [userActionClient]
    → paymentManager.createCheckout()   [facade → active provider]
    → returns session.url
  → window.location = session.url
  → user pays on provider-hosted checkout
  → POST /api/webhooks/<provider>   [Node runtime]
    → verifyWebhook() — signature check via node:crypto
    → handler → idempotency check on payment.sessionId
    → DB transaction:
        INSERT payment row
        addCredits()  // grants the right amount in same tx
  → user redirected to /settings/billing?status=success
```

## See also

* [Configuration](/configuration) — `siteConfig` knobs.
* [Customization](/customization) — common recipes.
* [Vercel deployment](/deployment/vercel) — ship the architecture.
* [Env reference](/env-reference) — every key that selects a provider.
