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

技术栈

目录结构

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。
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.tssrc/lib/auth.tssrc/lib/server.tssrc/lib/safe-action.ts、所有 src/payment/provider/*src/mail/index.tssrc/credits/index.tssrc/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.tsai.schema.tsschema.ts 统一 re-export 给 Drizzle Kit 和 Better Auth adapter。id 用 text(nanoid 前缀或 snowflake)—— 不用 serial,因为 text id 让你换 provider 时不破坏外键,也不会通过枚举泄露用户数。所有外键 在用户删除时级联(开箱即合 GDPR)。

双语 MDX

每篇文档两个平行文件:path/to/doc.mdxpath/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/linknext/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。

数据流——结账

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

另见