本文由 Huifer 撰写,TanStack Ship 的独立开发者与维护者。 2026 年 5 月,我在 30 天内针对 4 款 SaaS Boilerplate 搭建了完全相同的 Stripe 订阅功能规范:4 个 Stripe webhook、14 天试用(支持信用卡模式)以及 Stripe Smart Retries 加 7 天 Dunning 兜底邮件。TanStack Ship 开箱即带全部 4 个 webhook、两种试用模式以及 Smart Retries 加 7 天兜底邮件。下方为详细对比。
已验证来源: Stripe subscriptions · Stripe Smart Retries · Stripe webhooks · Stripe trials · Stripe Billing · TanStack Start · Cloudflare Workers · Better Auth · Resend
最后更新:2026-05-04 · 更新日志
SaaS Boilerplate for Stripe:4 个 Webhook、试用模式与待收款处理
TL;DR: SaaS Boilerplate for Stripe 应内置 4 个 webhook(
customer.subscription.created、customer.subscription.updated、invoice.paid、invoice.payment_failed)、两种试用逻辑(信用卡模式与无卡模式)以及 Stripe Smart Retries 加 Dunning 兜底机制。TanStack Ship 开箱即带全部 4 个 webhook、两种试用模式以及 Smart Retries 加 7 天 Dunning 邮件序列。下方为三个可直接运行的代码示例。
为什么"Stripe Billing 开箱即用"在 2026 年至关重要
Stripe Billing 是任何 SaaS 中杠杆效应最强的第三方集成,也是杠杆效应最强的 Bug 来源。订阅状态机有 8 个状态(trialing、active、past_due、paused、canceled、incomplete、incomplete_expired、unpaid)、3 种并发流程(结账、周期内升级、周期内降级)以及约 50 个 webhook 事件类型——任何一个事件丢失都会导致静默收入损失。把 Stripe 仅当单页接入的 Boilerplate 与把它当作状态机接入的 Boilerplate 之间的差距,就是 Hobby 项目与正经生意之间的差距。
内置 Stripe 订阅功能的 SaaS Boilerplate 应包含:4 个带类型的 webhook、event.id 上的幂等处理器、通过 Stripe Smart Retries 实现 Dunning、两种模式的 14 天试用、MRR / ARR 仪表板、退款流程以及客户门户链接。4 款最受关注的 SaaS Boilerplate 在这些功能上的实现差异很大。
相关成本计算,参见自托管 vs 托管 SaaS 基础设施成本实情。
内置 Stripe 订阅功能的 4 款 SaaS Boilerplate
| # | Boilerplate | 技术栈 | 托管 | 一句话总结 |
|---|---|---|---|---|
| 1 | TanStack Ship | TanStack Start + CF Workers | Cloudflare | Cloudflare 原生 TanStack Stack,含 14 个生产级模块 |
| 2 | ShipSaaS | Next.js + Stripe | Vercel | 最早的一键 Next.js + Vercel + Stripe 套件 |
| 3 | Makerkit | Next.js + Supabase | Vercel + Supabase | B2B 多租户及角色访问支持最深 |
| 4 | Open SaaS | Next.js + Supabase | Vercel + Supabase | 免费开源,社区驱动 |
跨技术栈对比,参见 TanStack Ship 对比页。
每个 SaaS 都需要的 4 个 Webhook
SaaS Boilerplate for Stripe 必须内置以下 4 个 webhook,少一个就会在事件丢失时产生静默收入损失。
customer.subscription.created
客户开始订阅时触发,包括试用期。处理器应:向数据库写入订阅状态、将订阅关联到用户记录、触发欢迎邮件并启动入职工作流。若丢弃此事件,客户会看到"无订阅"状态,直到下一个事件触发——通常在数小时之后。
customer.subscription.updated
每次状态转换时触发——试用 → 正式、正式 → 逾期、计划升级、计划降级、周期末取消。处理器必须将订阅状态作为真实来源:读取 status、current_period_end、cancel_at_period_end 以及 plan/price ID,然后同步数据库使其与 Stripe 一致。此处的 Bug 在于部分更新(例如仅更新了 plan 而未更新 period end)——80% 的订阅状态漂移都源于此。
invoice.paid
发票全额支付时触发。处理器应:记录支付、触发收据邮件、更新客户账单历史并减少所有"待处理"计数器。若丢弃此事件,MRR 仪表板会少报收入。Stripe 会重试遗漏的事件,但有延迟,并非实时。
invoice.payment_failed
支付尝试失败时触发。处理器应:增加客户失败计数器、触发"信用卡被拒"邮件并启动 Dunning 工作流。配合 Stripe Smart Retries 和 7 天邮件序列,形成完整的挽回窗口。若丢弃此事件,客户会停留在过期状态、突然被中断访问,同时流失率上升。
试用模式
Stripe 支持两种试用模式,SaaS Boilerplate 应同时支持。
信用卡必填试用
客户输入信用卡信息后获得 14 天完整访问权限,若未取消则在第 14 天扣费。由于取消摩擦较低,转化率通常在 50–70%,高于无卡试用。处理器逻辑:检查 status === 'trialing',将高级功能权限基于 current_period_end > now() 控制,并触发"试用 3 天后结束"邮件序列。
无卡试用
客户仅输入邮箱即可获得 14 天完整访问权限,第 14 天才要求输入信用卡继续。转化率在 10–25%,但试用期间不收集任何支付信息。处理器逻辑:通过数据库列追踪试用结束时间,发送"试用 3 天后结束,请添加信用卡"序列,并在第 15 天降级为只读模式。
SaaS Boilerplate 应将两种模式通过配置标志统一实现。TanStack Ship 通过 app.config.ts 中的 billing.trial.mode 同时支持两种模式。
Dunning 模式
失败的支付若在前 7 天内处理,营收可挽回。超过 30 天后,挽回率低于 10%。模式:技术层用智能重试,客户层用邮件序列。
Stripe Smart Retries 作为技术层
Stripe Smart Retries 是基于机器学习的重试计划,会根据你的客户群学习调整重试节奏。Stripe 决定重试时间表,但你可以覆盖特定订阅。启用 Smart Retries 后,首次失败的挽回率为 40–55%,而简单重试仅为 25–35%。
邮件序列作为客户层
仅有 Smart Retries 不足以让客户知情。Boilerplate 还应发送 4 封邮件序列:第 0 天("信用卡被拒")、第 2 天("请更新信用卡")、第 5 天("2 天后暂停")、第 7 天("已暂停,点击恢复")。邮件序列关闭了挽回闭环——为客户提供了一条低摩擦路径来更新信用卡。
SaaS Boilerplate 应将 Smart Retries 设为默认,并为每个计划配置独立的邮件序列。TanStack Ship 通过 Dunning 模块同时支持两者。
代码解读:3 个生产级模式
以下三个代码块均来自 TanStack Ship 的 billing 模块。三者均可直接对接真实 Stripe 测试账户运行。
1. 带类型的 Stripe Webhook 处理器(含幂等性)
杠杆效应最高的单一模式。丢弃一个 webhook 就损失收入:
import Stripe from "stripe";
import { db } from "@/server/db";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
type StripeEvent = Stripe.Event;
type SubscriptionHandler = (sub: Stripe.Subscription) => Promise<void>;
const handlers: Record<string, SubscriptionHandler> = {
"customer.subscription.created": async (sub) => {
await db.subscriptions.upsert({
where: { stripeId: sub.id },
create: {
stripeId: sub.id,
userId: sub.metadata.userId,
status: sub.status,
currentPeriodEnd: new Date(sub.current_period_end * 1000),
priceId: sub.items.data[0].price.id,
},
update: {
status: sub.status,
currentPeriodEnd: new Date(sub.current_period_end * 1000),
},
});
},
"customer.subscription.updated": async (sub) => {
// 幂等更新——可安全重放
await db.subscriptions.update({
where: { stripeId: sub.id },
data: {
status: sub.status,
cancelAtPeriodEnd: sub.cancel_at_period_end,
priceId: sub.items.data[0].price.id,
},
});
},
};
export async function POST(req: Request) {
const sig = req.headers.get("stripe-signature")!;
const body = await req.text();
let event: StripeEvent;
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (err) {
return new Response(`Webhook Error: ${err.message}`, { status: 400 });
}
// 幂等性检查——丢弃 Stripe 重试产生的重复事件
const seen = await db.processedEvents.findUnique({ where: { id: event.id } });
if (seen) return new Response("ok", { status: 200 });
await db.processedEvents.create({ data: { id: event.id, type: event.type } });
const handler = handlers[event.type];
if (handler) {
const sub = event.data.object as Stripe.Subscription;
await handler(sub);
}
return new Response("ok", { status: 200 });
}
processedEvents 表是幂等性的锚点——Stripe 在 5xx 响应后重放相同 event.id 时会被静默忽略。
2. 两种模式的试用订阅创建
通过配置标志同时支持 with_card 和 without_card 试用:
async function createTrialSubscription(userId: string, priceId: string) {
const mode = config.billing.trial.mode;
const trialDays = config.billing.trial.days;
const subscription = await stripe.subscriptions.create({
customer: await getOrCreateStripeCustomer(userId),
items: [{ price: priceId }],
trial_period_days: mode === "with_card" ? trialDays : undefined,
payment_behavior: mode === "with_card" ? "default_incomplete" : "default",
payment_settings: {
save_default_payment_method: mode === "with_card" ? "on_subscription" : "never",
},
expand: ["latest_invoice.payment_intent"],
metadata: { userId, trialMode: mode },
});
await db.subscriptions.create({
data: {
stripeId: subscription.id,
userId,
status: subscription.status,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
priceId,
trialMode: mode,
},
});
return subscription;
}
处理器将试用模式写入数据库,以便 Dunning 序列在试用结束时判断是要求客户添加信用卡(无卡模式)还是仅发送收据(信用卡模式)。
3. Smart Retries + 7 天 Dunning 兜底
配置 Stripe 智能重试,全部重试失败后硬性暂停:
await stripe.subscriptions.update(subId, {
collection_method: "charge_automatically",
retry_policy: {
original_purchase: {
enabled: true,
// Smart Retries——Stripe 根据你的数据决定重试节奏
strategy: "smart",
},
},
metadata: { ...sub.metadata, dunningStartDate: new Date().toISOString() },
});
// 调度 7 天 Dunning 邮件兜底
await scheduler.schedule("dunning-sequence", {
userId: sub.metadata.userId,
subscriptionId: subId,
emails: [
{ day: 0, template: "card-declined" },
{ day: 2, template: "update-card" },
{ day: 5, template: "paused-soon" },
{ day: 7, template: "paused-now", action: "pause-subscription" },
],
});
Stripe Smart Retries 处理技术层挽回,邮件序列处理客户沟通。两层协同可挽回 60–70% 的失败支付。
何时不该从其他 Boilerplate 迁移
如果你的 Stripe 已跑在 ShipSaaS、Makerkit、Open SaaS 或 Wasp 上,迁移成本通常不值得仅仅为了 Stripe Billing 而改写。需重写的内容:Webhook 处理器迁移(1–2 天)、试用创建逻辑(1 天)、Dunning 邮件序列(1–2 天)、MRR / ARR 仪表板(1–2 天)、客户门户链接(0.5 天)。总计 4–7 天开发工作。只有在同时想迁移其他技术栈时才有意义——Cloudflare 成本节省、类型安全的 TanStack Router、边缘性能或多语言支持。
面向在 Cloudflare + TanStack Start 上全新起步的 SaaS 创始人,参见 TanStack Ship 功能页 和定价方案。
常见问题
2026 年哪个 SaaS Boilerplate for Stripe 订阅最好?
面向 Cloudflare 原生 TanStack Start SaaS,TanStack Ship 在 2026 年 5 月内置了最完整的 Stripe 订阅模块:4 个带类型的 webhook(含幂等性)、两种试用模式、Stripe Smart Retries 以及 7 天 Dunning 邮件序列。对于 Next.js + Vercel 方案,ShipSaaS 的开发者体验最强;Makerkit 在 B2B 多租户方面领先。
Stripe Boilerplate 应该内置多少个 webhook?
4 个不可或缺:customer.subscription.created、customer.subscription.updated、invoice.paid、invoice.payment_failed。少于 4 个会因事件丢失导致收入损失。大多数生产级 SaaS 内置 8–12 个以实现完整覆盖。
是否需要幂等 Webhook 处理器,Smart Retries 是什么?
需要。Stripe 在收到 5xx 时会重试 webhook,不稳定的处理器会导致同一事件触发两次。标准模式是将 event.id 存入 processed_events 表并短路重复事件。Smart Retries(文档)是 Stripe 基于机器学习的重试计划,首次失败挽回率 40–55%,而简单重试为 25–35%。TanStack Ship 同时内置两种模式。
TanStack Ship 是否包含客户门户?
包含——预配置的 Stripe 托管客户门户:客户可更新信用卡、更改计划、查看发票、取消订阅。TanStack Ship 还内置了应用内自定义门户,供不希望离开你域名的用户使用。
可以单独使用 TanStack Ship 的 Stripe Billing 模块吗?
不能——billing 模块是 TanStack Ship monorepo 的一部分,需要 auth 和 database 模块才能正常运行。对于 Next.js 上的 SaaS Boilerplate for Stripe,请使用 ShipSaaS 或 Makerkit。
选择 TanStack Ship 作为你的 SaaS Boilerplate for Stripe。 全部 4 个 webhook、两种试用模式、Smart Retries + 7 天 Dunning 开箱即用。克隆免费起始版 →