
市面上流行的 Next.js SaaS 模板是一类特殊的开源产物:它不是给你读的示例项目,而是卖给要交付真实业务的人的起点。以 ShipFast 为例,公开口径是一次性买断、累计约 1.27M 美元、单人维护、2023 年 9 月上线。这个形态决定了它的代码组织围绕「被千余人 fork 后各自魔改」来设计,和普通开源项目的维护逻辑差别很大。本文不引用其内部代码,只讨论这类模板通用的工程决策与背后的理由。
一、目录结构:为什么业务页面总是「薄」的
模板型仓库常见的分层是 app/(页面编排)、components/(展示)、lib/ 或 packages/(能力模块)。页面文件通常只有几十行:取会话、取数据、渲染。逻辑全部下沉到能力模块,原因是模板的买家不会改你的认证实现,但一定会改落地页文案——把易变的东西放在浅层、把稳定的东西封在深层,才能让买家升级模板版本时冲突最小。
这也解释了一个常见设计:环境变量集中在一个 siteConfig / schema 文件里,启动时校验。模板的买家最常犯的错是配了一半就部署,支付回调跑到一半才发现缺 key。把配置契约前置到启动阶段,是模板作者用无数售后问题换来的经验。
二、认证模块:身份与账号分离的两层模型
自建认证或封装 NextAuth 时,模板普遍采用两层模型:accounts 表存 OAuth provider 与 providerAccountId 的映射,users 表存业务身份。会话里只带内部 userId。
// 通用示意:provider 只证明身份,业务表决定"你是谁"
const account = await db.account.upsert({
where: {
provider_providerAccountId: {
provider, providerAccountId } },
update: {
accessToken },
create: {
provider, providerAccountId, userId },
});
这样设计的原因是模板买家会频繁更换登录方式:今天用 GitHub 登录,明天想加 Google。如果 OAuth 身份直接当用户主键,换 provider 就等于丢号。两层模型把「解绑/合并/换绑」变成纯数据操作,不需要动会话逻辑。
会话策略上,这类模板多选 database session 而不是纯 JWT:多一次数据库查询,换来服务端可撤销。单人维护的付费产品,遇到「帮客户强制下线」这类工单时,能撤销的会话比省一次查询值钱得多。
三、支付模块:把「收到钱」和「给权限」拆成两个问题
一次性买断模式下的支付模块,核心不是调 Stripe SDK,而是权益(entitlement)建模。通用的拆法是三段:
- 事实层:Stripe webhook 是唯一事实源。验签、读原始 body、按 event.id 做幂等去重。
- 权益层:一张 entitlement 表记录「哪个用户拥有什么」,退款、争议、手动补单都落到同一张表,不散落在订单状态字段里。
- 门禁层:业务代码只查 entitlement,永远不直接问「付没付款」。
// 幂等消费 webhook 的通用骨架
const processed = await db.webhookEvent.findUnique({
where: {
id: event.id } });
if (processed) return new Response("ok");
await db.webhookEvent.create({
data: {
id: event.id, type: event.type } });
// 之后才允许触发权益变更
为什么买断制也要这么认真?因为买断的售后场景比订阅更碎:换设备、送副本、退款后收回访问权。订阅模型天然有周期状态兜底,买断模型只有你自己的权益表。模板作者把这块封装好,买家就不必每人重踩一遍。
四、邮件模块:outbox 模式是小团队的性价比解
注册验证、付款回执这类事务邮件,最常见的错误写法是在数据库事务里直接 await 邮件 API。SMTP 抖动 30 秒,事务挂 30 秒;邮件供应商限流,业务直接报错。outbox 模式把「业务发生了」和「邮件要发出去」拆开:同一个事务里写业务表和 outbox 表,独立进程消费 outbox 投递邮件,失败指数退避,超限进死信。
对小团队而言,这个模式的性价比在于:它只需要一张表加一个轮询 worker,不引入消息队列中间件。模板项目把投递通道抽象成接口(SMTP 或 API 供应商可替换),买家换邮件服务商时只改一行配置。发件域的 SPF/DKIM/DMARC 配置则属于模板文档必须覆盖的部分——代码再好,进垃圾箱一切白搭。
五、从维护期反推的工程取舍
公开数据显示这类产品进入维护期后,收入占比会明显下滑(该案例从月收入约 40% 降到约 3.5%)。这不是失败,而是买断制模板的常态:收入前置、维护长尾。反映到代码上,模板作者的理性策略是:
- 公开接口保持极稳,内部实现随意重构——买家的分叉越深,破坏性变更的代价越高;
- 升级靠 codemod 和迁移文档,不靠人肉 diff;
- 模块间只通过窄接口通信,让买家可以只替换其中一个模块。
对在自己的产品里借鉴这套结构的人来说,真正值得抄的不是某个具体函数,而是这些由「千余人各自魔改」的约束逼出来的边界设计。
本文基于公开可查的产品数据讨论通用工程实践,不涉及该产品内部实现。