> ## 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.

# 架构

> Vibestrap 是怎么搭起来的——技术栈、目录、和那些撑住整体的关键决定。

Vibestrap 是一个 Next.js 15 应用，TypeScript strict、Drizzle on Postgres、
Better Auth 管 session、next-intl 做 i18n、Tailwind v4 出样式。设计目标是
「一个周末把付费 SaaS 上线」——每个架构决定都为这个目标服务，并且有一小撮关键决定撑住整体。
本页就是那一小撮。

## 技术栈

| 层       | 技术                                                       | 为什么                                    |
| ------- | -------------------------------------------------------- | -------------------------------------- |
| 框架      | Next.js 15（App Router、RSC、Turbopack）                     | server-first 渲染、生态成熟。                  |
| Runtime | React 19                                                 | async server components、`use()` hook。  |
| 语言      | TypeScript 5（strict）                                     | 上线前抓住类型形状错误。                           |
| 样式      | Tailwind v4 + shadcn/ui                                  | OKLCH 色彩、`globals.css` 里 CSS-first 配置。 |
| ORM     | Drizzle                                                  | 类型化 schema、无 codegen、可写裸 SQL。          |
| 数据库     | 仅 Postgres                                               | 真相单一来源、事务、JSONB。                       |
| 认证      | Better Auth + Drizzle adapter                            | 服务端 session、OAuth、邮件验证。                |
| i18n    | next-intl                                                | EN/ZH 双语，URL 前缀 `as-needed`。           |
| 文档      | Fumadocs MDX                                             | 双语 `*.zh.mdx`，按 locale 查找。             |
| 支付      | Stripe / Creem / NOWPayments                             | 同一个 facade 后面。                         |
| AI      | Mock / OpenRouter / OpenAI / Anthropic / Replicate / fal | 同样的 facade 模式；mock 离线可跑。               |
| 邮件      | Resend + React Email                                     | 模板化、类型安全、dev 可预览。                      |

## 目录结构

```
vibestrap/
├── content/                 # MDX 内容（双语）
│   ├── docs/                # *.mdx + *.zh.mdx 平行对
│   ├── blog/
│   └── changelog/
├── messages/                # next-intl 文案 bundle
│   ├── en.json
│   └── zh.json
├── public/                  # 静态资源、OG 图、logo
├── drizzle/                 # 生成的 SQL migration（已 commit）
├── src/
│   ├── app/                 # Next.js App Router
│   │   ├── [locale]/        # 带 locale 前缀的路由
│   │   │   ├── (marketing)/ # 公共页——home、pricing、blog、docs
│   │   │   ├── (auth)/      # login / register / forgot-password
│   │   │   └── (app)/       # 已登录——settings、admin、dashboard
│   │   ├── api/             # route handler（auth、webhooks、ping）
│   │   ├── layout.tsx       # 根 html、字体、主题
│   │   └── globals.css      # Tailwind v4 @theme + tokens
│   ├── components/          # blocks、layout、ui（shadcn）、features
│   ├── config/site.ts       # 中心配置，< 250 行
│   ├── db/                  # 拆分 schema：auth / app / affiliate / ai / license
│   ├── payment/             # facade + 4 个 provider + webhook handlers
│   ├── ai/                  # facade + 5 个 provider + cost / pricing
│   ├── credits/             # 4 类账目（仅服务端）
│   ├── mail/                # facade + Resend + React Email 模板
│   ├── newsletter/          # facade + Resend / Beehiiv
│   ├── customer-service/    # widget loader（Crisp / Tawk / Intercom / Chatwoot）
│   ├── affiliate/           # script loader + 内部追踪
│   ├── analytics/           # GA / PostHog / Plausible / Umami 扇出
│   ├── i18n/                # routing.ts + request.ts + 导航助手
│   ├── lib/                 # auth、safe-action、server、utils
│   ├── env.ts               # zod 校验过的 env 变量
│   └── middleware.ts        # i18n 路由 + 认证守卫
├── wrangler.toml.example    # Cloudflare Workers 配置模板
└── package.json
```

`app/`（在 `src/` 里）是路由表面。`src/` 里 `app/` 之外的模块是页面和 route handler 调用的业务逻辑。
`app/[locale]/` 段是 i18n 路由发生的地方；`(marketing)`、`(auth)`、`(app)` 这些 route group
是为了 layout 分组和认证守门，不影响 URL 形状。

## 关键决定

### Provider facade 模式

每个对接外部服务的模块都导出**一个对象**（facade），背后是**可互换的 provider**。
facade 在启动时从 `siteConfig.<module>.provider` 选实现。消费者只见 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];
}
```

`src/ai/`、`src/mail/`、`src/newsletter/`、`src/customer-service/`、`src/affiliate/`
都是同一套。给 mail 加 Postmark 就是 `src/mail/provider/postmark.ts` 一个新文件加一条 switch
分支——消费者代码一行不改。

### server-only 强制

碰 DB 或读 secret 的文件在顶部 `import 'server-only'`。这种模块要是被 client component 引用，
Next.js build 会失败。已保护：`src/db/index.ts`、`src/lib/auth.ts`、`src/lib/server.ts`、
`src/lib/safe-action.ts`、所有 `src/payment/provider/*`、`src/mail/index.ts`、
`src/credits/index.ts`、`src/credits/server.ts`。新建仅服务端模块时记得加这一行——
半秒钟敲完，省掉一次意外泄密。

### 三档 server actions

所有写操作走 `next-safe-action`，三种 client 在 `src/lib/safe-action.ts`：

* `actionClient` —— 公共，不需要认证。
* `userActionClient` —— 需要登录；提供 `ctx.user`。
* `adminActionClient` —— 需要登录；要求 `user.role === 'admin'`。

每个 action 用 Zod 声明输入 schema。输出端到端类型化。守门集中——不必在每个 action
散落 `if (!user) throw` 样板。

### 幂等 webhook

支付 webhook 在任何非 2xx 响应时都会重试。如果你在重试时重复发积分，就等于一笔付款付了两次。
脚手架在 `src/payment/handlers/` 里靠两条保证解决：

1. `payment.invoiceId` 有 unique index——重复 insert 会让事务失败。
2. 每个 handler 在 insert 前先做 `payment.sessionId` 查询，命中就早退。

发积分和 insert payment 行**在同一个事务里**——要么一起落，要么一起不落。加新事件类型
（订阅续费、退款）时照这个形状写——绝不要把发积分单独再调一次。

### 拆分的 DB schema

`src/db/` 按职责拆开：`auth.schema.ts`（Better Auth）、`app.schema.ts`（payment + credits +
transactions）、`affiliate.schema.ts`、`ai.schema.ts`。`schema.ts`
统一 re-export 给 Drizzle Kit 和 Better Auth adapter。id 用 `text`（nanoid 前缀或 snowflake）——
不用 `serial`，因为 text id 让你换 provider 时不破坏外键，也不会通过枚举泄露用户数。所有外键
在用户删除时级联（开箱即合 GDPR）。

### 双语 MDX

每篇文档两个平行文件：`path/to/doc.mdx` 和 `path/to/doc.zh.mdx`。Fumadocs 配置成先找带 locale
后缀的文件，找不到回落默认。同一套机制用于 blog 文章和 changelog。两种 locale 都存在才算上线。

### Locale 路由——`as-needed` 前缀

`src/i18n/routing.ts` 把 next-intl 配成英文留在根（`/about`）、中文加前缀（`/zh/about`）。
全程用 `@/i18n/navigation` 里的 `Link` / `useRouter`——它们自动保留 locale 前缀。
直接 import `next/link` 或 `next/navigation` 会在客户端导航时悄悄丢掉 locale。

### 钱用 micro-cent

AI 推理和积分账务都涉及小于一分钱的数。用 float 存钱招舍入 bug；用整数 **cent** 存又丢亚分精度
（百万 token 的 Claude 调用每 token 价 \$0.000003）。脚手架统一存整数 **micro-cent**（百万分之一
USD），到边缘再换显示单位。算法见 `src/ai/pricing.ts`。

### Edge runtime —— 只给 OG 图

`src/app/opengraph-image.tsx` 跑在 Edge runtime，因为这是唯一一处 cold start 直接影响用户可见
性能的路径（社交平台抓图器超时很快）。其他全部——包括所有访问 Postgres 的 API 路由——用 **Node**
runtime，因为 `pg` 驱动和不少 auth 依赖需要完整 Node API。除非你清楚在让出什么，否则别把路由
搬到 Edge。

## 数据流——结账

一个穿透大半个架构的典型流程：

```
用户点击「$49 立即获取 Vibestrap」
  → /register?plan=vibestrap-promo
  → 注册后 → /settings/billing
  → createCheckoutAction({plan})    [userActionClient]
    → paymentManager.createCheckout()   [facade → 当前 provider]
    → 返回 session.url
  → window.location = session.url
  → 用户在 provider 托管页支付
  → POST /api/webhooks/<provider>   [Node runtime]
    → verifyWebhook() —— 用 node:crypto 校签名
    → handler → 用 payment.sessionId 做幂等检查
    → DB 事务：
        INSERT payment 行
        addCredits()  // 同事务里发对应数量
  → 用户跳回 /settings/billing?status=success
```

## 另见

* [配置](/zh/configuration) —— `siteConfig` 旋钮。
* [自定义](/zh/customization) —— 常见 recipes。
* [Vercel 部署](/zh/deployment/vercel) —— 把架构上线。
* [Env 参考](/zh/env-reference) —— 选 provider 用的每个 key。
