Stripe
Stripe 插件,用于 Better Auth,管理订阅和支付。
Stripe 插件将 Stripe 的支付与订阅功能集成到 Better Auth 中。由于支付和认证通常紧密相关,此插件简化了在应用中集成 Stripe 的流程,负责客户创建、订阅管理和 webhook 处理。
功能
- 用户注册时自动创建 Stripe 客户
- 管理订阅计划和价格
- 处理订阅生命周期事件(创建、更新、取消)
- 安全处理 Stripe webhook,带签名验证
- 向应用暴露订阅数据
- 支持试用期和订阅升级
- 自动防止试用滥用 — 用户在所有计划中每个账户只可获一次试用
- 灵活的引用系统,关联订阅与用户或组织
- 支持团队订阅和座位管理
安装
在 auth 配置中添加插件
import { betterAuth } from "better-auth"
import { stripe } from "@better-auth/stripe"
import Stripe from "stripe"
const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2025-11-17.clover", // Stripe SDK v20.0.0 支持的最新 API 版本
})
export const auth = betterAuth({
// ... 你已有的配置
plugins: [
stripe({
stripeClient,
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
createCustomerOnSignUp: true,
})
]
})从 Stripe v18 升级? 版本 19 采用了异步的 webhook 签名验证(constructEventAsync),插件内部已处理,无需你改动代码!
添加客户端插件
import { createAuthClient } from "better-auth/client"
import { stripeClient } from "@better-auth/stripe/client"
export const authClient = createAuthClient({
// ... 你已有的配置
plugins: [
stripeClient({
subscription: true // 如果启用订阅管理
})
]
})设置 Stripe webhook
在 Stripe 仪表盘创建 webhook,指向:
https://your-domain.com/api/auth/stripe/webhook/api/auth 是认证服务器的默认路径。
确保至少选择以下事件:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deleted
保存 Stripe 提供的 webhook 签名密钥,作为环境变量 STRIPE_WEBHOOK_SECRET。
使用
客户管理
你可以仅使用此插件进行客户管理,而不启用订阅。适合你仅想关联 Stripe 客户和用户的场景。
当设置 createCustomerOnSignUp: true 时,用户注册时会自动创建 Stripe 客户,并与数据库中的用户关联。你也可定制客户创建流程:
stripe({
// ... 其他选项
createCustomerOnSignUp: true,
onCustomerCreate: async ({ stripeCustomer, user }, ctx) => {
// 处理新创建的客户
console.log(`客户 ${stripeCustomer.id} 已为用户 ${user.id} 创建`);
},
getCustomerCreateParams: async (user, ctx) => {
// 定制 Stripe 客户创建参数
return {
metadata: {
referralSource: user.metadata?.referralSource
}
};
}
})订阅管理
定义计划
你可以静态定义订阅计划,也可动态获取:
// 静态定义
subscription: {
enabled: true,
plans: [
{
name: "basic", // 计划名称,存入数据库时自动小写
priceId: "price_1234567890", // Stripe 价格 ID
annualDiscountPriceId: "price_1234567890", // (可选)年付折扣价格 ID
limits: {
projects: 5,
storage: 10
}
},
{
name: "pro",
priceId: "price_0987654321",
limits: {
projects: 20,
storage: 50
},
freeTrial: {
days: 14,
}
}
]
}
// 动态定义(数据库或接口获取)
subscription: {
enabled: true,
plans: async () => {
const plans = await db.query("SELECT * FROM plans");
return plans.map(plan => ({
name: plan.name,
priceId: plan.stripe_price_id,
limits: JSON.parse(plan.limits)
}));
}
}详情见 计划配置。
创建订阅
通过调用 subscription.upgrade 方法创建订阅:
const { data, error } = await authClient.subscription.upgrade({ plan: "pro", // required annual: true, referenceId: "123", subscriptionId: "sub_123", metadata, customerType, seats: 1, locale, successUrl, // required cancelUrl, // required returnUrl, disableRedirect: false, // required scheduleAtPeriodEnd: false,});planstringrequired要升级的计划名称。
annualboolean是否升级为年付计划。
referenceIdstring订阅的引用 ID。默认根据 customerType 赋值。
subscriptionIdstring要升级的订阅 ID。
metadataRecord<string, any>附加元数据,存储于订阅。
customerType"user" | "organization"计费的客户类型。(默认:"user")
seatsnumber要升级的座位数(如适用)。
localestring显示 Checkout 的 IETF 语言标签。未提供或设为 auto 时使用浏览器语言。
successUrlstringrequired支付或设置完成后 Stripe 重定向的 URL。
cancelUrlstringrequired若设置,Checkout 显示返回按钮,取消支付时重定向此处。
returnUrlstring从账单门户返回的 URL(用于升级现有订阅)
disableRedirectbooleanrequired成功订阅后禁用自动重定向。
scheduleAtPeriodEndboolean在当前计费周期结束时调度计划变更,而不是即时生效。
简单示例:
await authClient.subscription.upgrade({
plan: "pro",
successUrl: "/dashboard",
cancelUrl: "/pricing",
annual: true, // 可选:升级为年付计划
referenceId: "org_123", // 可选:默认为 customerType 的值
seats: 5, // 可选:团队计划座位数
locale: "en" // 可选:用英文显示 Checkout
});这会创建一个 Checkout 会话,并将用户重定向至 Stripe Checkout 页面。
插件只支持每个引用 ID(用户或组织)最多一个活跃或试用订阅,不支持同一引用并发多订阅。
如果用户已有活跃订阅,切记升级时必须提供 subscriptionId 参数,否则可能同时创建新订阅,导致重复计费。
重要提示:
successUrl参数会在内部被修改,以处理结账完成与 webhook 处理之间的竞态条件。插件会创建跳转中转页面,确保订阅状态被正确更新后再跳转到你指定的成功页。
const { error } = await authClient.subscription.upgrade({
plan: "pro",
successUrl: "/dashboard",
cancelUrl: "/pricing",
});
if(error) {
alert(error.message);
}切换计划
想切换订阅计划,可以调用 subscription.upgrade:
await authClient.subscription.upgrade({
plan: "pro",
successUrl: "/dashboard",
cancelUrl: "/pricing",
subscriptionId: "sub_123", // 当前用户计划的 Stripe 订阅 ID
});确保用户仅支付新计划,不会同时支付两个计划费用。
在周期结束时调度计划更改
默认计划调整立即生效,且自动按比例计费。若希望用户继续使用当前计划直到结算周期结束,再切换计划,可以这样:
await authClient.subscription.upgrade({
plan: "pro",
successUrl: "/dashboard",
cancelUrl: "/pricing",
returnUrl: "/billing",
scheduleAtPeriodEnd: true, // 默认 false
});此方案利用 Stripe 订阅调度 API,创建两阶段执行:当前计划持续至周期结束,然后自动启动新计划,且不会按比例计费。
当 scheduleAtPeriodEnd 为 true 时:
- 订阅计划保持不变直到周期结束,仅存储
stripeScheduleId以供客户端检测待切换状态 - 不会跳转到 Stripe Checkout 或账单门户,调整由服务端执行
- 计费周期结束后 Stripe 会触发
customer.subscription.updatedwebhook,自动更新订阅记录 - 若周期前请求新的升级或调度,先释放已有挂起调度
列出活跃订阅
获取用户的活跃订阅:
const { data: subscriptions, error } = await authClient.subscription.list({ query: { referenceId: '123', customerType, },});// 获取活跃订阅const activeSubscription = subscriptions.find( sub => sub.status === "active" || sub.status === "trialing");// 检查订阅限制const projectLimit = subscriptions?.limits?.projects || 0;referenceIdstring要列出的订阅引用 ID。
customerType"user" | "organization"计费客户类型。(默认:"user")
确保插件配置中加入了 authorizeReference 以授权引用 ID:
stripe({
// ... 其他选项
subscription: {
// ... 其他订阅配置
authorizeReference: async ({ user, session, referenceId, action }) => {
if(action === "list-subscription") {
const org = await db.member.findFirst({
where: {
organizationId: referenceId,
userId: user.id
}
});
return org?.role === "owner"
}
// 检查用户是否有权限列出此引用的订阅
return true;
}
}
})取消订阅
取消订阅:
const { data, error } = await authClient.subscription.cancel({ referenceId: 'org_123', customerType, subscriptionId: 'sub_123', returnUrl: '/account', // required});referenceIdstring要取消的订阅引用 ID。默认根据 customerType 赋值。
customerType"user" | "organization"计费客户类型。(默认:"user")
subscriptionIdstring要取消的订阅 ID。
returnUrlstringrequired用户点击账单门户中的链接返回你网站时的跳转 URL。
将会重定向用户进入 Stripe 账单门户,让用户自行取消订阅。
取消状态说明
Stripe 支持多种取消状态,插件会跟踪它们:
| 字段 | 说明 |
|---|---|
cancelAtPeriodEnd | 若订阅状态为 active,表示将在当前计费期结束时取消;若状态为 canceled,表示已在期末取消。 |
cancelAt | 订阅计划被取消时生效的时间点(若已排期取消)。 |
canceledAt | 已取消订阅的请求时间。注意:若通过 cancelAtPeriodEnd 取消,此时间为取消请求时间,不是实际结束时间。 |
endedAt | 订阅真正结束的日期。 |
status | 仅当订阅真正结束后,才会切换为 "canceled" 状态。 |
恢复订阅
注意: 只支持活动订阅且处于待取消或待调度变更状态的恢复。已结束的订阅(status: "canceled"且endedAt已设)无法恢复。
用户取消或排期变更后改变主意,可以恢复订阅:
const { data, error } = await authClient.subscription.restore({ referenceId: '123', customerType, subscriptionId: 'sub_123',});referenceIdstring要恢复的订阅引用 ID。默认根据 customerType 赋值。
customerType"user" | "organization"计费客户类型。(默认:"user")
subscriptionIdstring要恢复的订阅 ID。
此接口处理两种情况:
- 待取消:取消期末生效的订阅,设置
cancelAtPeriodEnd为false,清除cancelAt和canceledAt,保证订阅续期继续。 - 待计划变更(通过
scheduleAtPeriodEnd):释放 Stripe 订阅调度,清除stripeScheduleId,保持当前计划不变。
创建账单门户会话
创建 Stripe 账单门户会话,让客户管理订阅、支付方式和账单历史:
const { data, error } = await authClient.subscription.billingPortal({ locale, referenceId: "123", customerType, returnUrl, disableRedirect: false,});localestring显示账单门户的 IETF 语言标签。未提供或 auto 时使用浏览器语言。
referenceIdstring订阅引用 ID。
customerType"user" | "organization"计费客户类型。(默认:"user")
returnUrlstring退出账单门户后跳转的 URL。
disableRedirectboolean禁用自动重定向到账单页面。 @default false
支持的语言标签请查看 IETF 语言标签文档。
该接口会创建 Stripe 账单门户会话,响应中以 data.url 返回,可以重定向用户至该地址进行订阅和支付管理。
引用系统
默认订阅与用户 ID 关联。你也可以自定义引用 ID,将订阅关联到其他实体,如组织:
// 为组织创建订阅
await authClient.subscription.upgrade({
plan: "pro",
referenceId: "org_123456",
successUrl: "/dashboard",
cancelUrl: "/pricing",
seats: 5 // 团队计划座位数
});
// 列出组织的订阅
const { data: subscriptions } = await authClient.subscription.list({
query: {
referenceId: "org_123456"
}
});团队订阅与座位数
团队或组织计划可以指定座位数:
await authClient.subscription.upgrade({
plan: "team",
referenceId: "org_123456",
seats: 10, // 10 个团队成员
successUrl: "/org/billing/success",
cancelUrl: "/org/billing"
});seats 参数会传给 Stripe 作为订阅品项数量,可用于应用逻辑中限制团队成员数。
要授权引用 ID,请实现 authorizeReference:
subscription: {
// ... 其他选项
authorizeReference: async ({ user, session, referenceId, action }) => {
// 检查用户是否有权限管理此引用的订阅
if (action === "upgrade-subscription" || action === "cancel-subscription" || action === "restore-subscription") {
const org = await db.member.findFirst({
where: {
organizationId: referenceId,
userId: user.id
}
});
return org?.role === "owner"
}
return true;
}
}Webhook 处理
插件自动处理常见 webhook 事件:
checkout.session.completed:结账完成后更新订阅状态customer.subscription.created:在非结账流程创建订阅时触发customer.subscription.updated:订阅更新时触发customer.subscription.deleted:标记订阅已取消
你也可以响应自定义事件:
stripe({
// ... 其他选项
onEvent: async (event) => {
// 处理任何 Stripe 事件
switch (event.type) {
case "invoice.paid":
// 处理已付款发票
break;
case "payment_intent.succeeded":
// 处理支付成功
break;
}
}
})订阅生命周期钩子
你可以挂载订阅各种生命周期事件回调:
subscription: {
// ... 其他选项
onSubscriptionComplete: async ({ event, subscription, stripeSubscription, plan }) => {
// 结账方式成功创建订阅时调用
await sendWelcomeEmail(subscription.referenceId, plan.name);
},
onSubscriptionCreated: async ({ event, subscription, stripeSubscription, plan }) => {
// 订阅在结账外创建时调用(比如后台仪表盘)
await sendSubscriptionCreatedEmail(subscription.referenceId, plan.name);
},
onSubscriptionUpdate: async ({ event, subscription }) => {
// 订阅更新时调用
console.log(`订阅 ${subscription.id} 已更新`);
},
onSubscriptionCancel: async ({ event, subscription, stripeSubscription, cancellationDetails }) => {
// 订阅取消时调用
await sendCancellationEmail(subscription.referenceId);
},
onSubscriptionDeleted: async ({ event, subscription, stripeSubscription }) => {
// 订阅删除时调用
console.log(`订阅 ${subscription.id} 已删除`);
}
}试用期
你可以为计划配置试用期:
{
name: "pro",
priceId: "price_0987654321",
freeTrial: {
days: 14,
onTrialStart: async (subscription) => {
// 试用期开始时调用
await sendTrialStartEmail(subscription.referenceId);
},
onTrialEnd: async ({ subscription }, ctx) => {
// 试用期结束时调用
await sendTrialEndEmail(subscription.referenceId);
},
onTrialExpired: async (subscription, ctx) => {
// 试用到期未转换时调用
await sendTrialExpiredEmail(subscription.referenceId);
}
}
}Schema
Stripe 插件会向数据库添加以下表:
User
表名:user
Organization
表名:organization (仅当 organization.enabled 为 true 时)
Subscription
表名:subscription
自定义 Schema
若需要更改表名或字段,可传入 schema 选项至 Stripe 插件:
stripe({
// ... 其他选项
schema: {
subscription: {
modelName: "stripeSubscriptions", // 将订阅表映射为 stripeSubscriptions
fields: {
plan: "planName" // 将 plan 字段映射为 planName
}
}
}
})选项
| 选项 | 类型 | 说明 |
|---|---|---|
stripeClient | Stripe | Stripe 客户端实例。必需。 |
stripeWebhookSecret | string | Stripe webhook 签名密钥。必需。 |
createCustomerOnSignUp | boolean | 用户注册时是否自动创建 Stripe 客户。默认:false。 |
onCustomerCreate | function | 客户创建后回调,参数 { stripeCustomer, user } 和上下文。 |
getCustomerCreateParams | function | 自定义 Stripe 客户创建参数,参数是 user 和上下文。 |
onEvent | function | 收到任何 Stripe webhook 事件时回调,参数为 Stripe.Event。 |
subscription | object | 订阅配置。详见下文 订阅选项。 |
organization | object | 启用组织客户支持。详见下文 组织选项。 |
schema | object | 自定义 Stripe 插件的数据库 schema。 |
订阅选项
| 选项 | 类型 | 说明 |
|---|---|---|
enabled | boolean | 是否启用订阅功能。必需。 |
plans | StripePlan[] 或 function | 订阅计划数组或异步返回计划的函数。启用时必需。 |
requireEmailVerification | boolean | 是否要求用户验证邮箱后才允许订阅升级。默认:false。 |
authorizeReference | function | 授权引用 ID。参数 { user, session, referenceId, action } 和上下文。 |
getCheckoutSessionParams | function | 自定义 Stripe Checkout 会话参数。参数 { user, session, plan, subscription },请求和上下文。 |
onSubscriptionComplete | function | 通过结账创建订阅时调用。参数 { event, stripeSubscription, subscription, plan } 和上下文。 |
onSubscriptionCreated | function | 在非结账流程创建订阅时调用。参数 { event, stripeSubscription, subscription, plan }。 |
onSubscriptionUpdate | function | 订阅更新时调用。参数 { event, subscription }。 |
onSubscriptionCancel | function | 订阅取消时调用。参数 { event, subscription, stripeSubscription, cancellationDetails }。 |
onSubscriptionDeleted | function | 订阅被删除时调用。参数 { event, stripeSubscription, subscription }。 |
计划配置
| 选项 | 类型 | 说明 |
|---|---|---|
name | string | 计划名称。必需。 |
priceId | string | Stripe 价格 ID。除非使用 lookupKey,否则必需。 |
lookupKey | string | Stripe 价格查找键。可替代 priceId。 |
annualDiscountPriceId | string | 年付计费价格 ID。 |
annualDiscountLookupKey | string | 年付计费价格查找键。 |
limits | object | 计划限制(如 { projects: 10, storage: 5 })。 |
group | string | 计划分组名称。 |
seatPriceId | string | 按座位计费价格 ID,需启用 organization 插件。 |
lineItems | LineItem[] | 额外的结账会话商品条目。 |
freeTrial | object | 试用配置,详见下文 免费试用配置。 |
Stripe Checkout 会话不支持 混合周期订阅。结账中所有商品条目必须使用相同计费间隔(如全部月付或全部年付)。若周期不同,Stripe API 会拒绝请求。
免费试用配置
| 选项 | 类型 | 说明 |
|---|---|---|
days | number | 试用天数。必需。 |
onTrialStart | function | 试用开始时调用,参数 subscription。 |
onTrialEnd | function | 试用结束时调用,参数 { subscription } 和上下文。 |
onTrialExpired | function | 试用过期且未转化时调用,参数 subscription 和上下文。 |
组织选项
| 选项 | 类型 | 说明 |
|---|---|---|
enabled | boolean | 启用组织客户支持。必需。 |
getCustomerCreateParams | function | 自定义组织的 Stripe 客户创建参数,参数为 organization 和上下文。 |
onCustomerCreate | function | 组织客户创建后调用,参数 { stripeCustomer, organization } 和上下文。 |
高级用法
与组织插件一起使用
Stripe 插件集成了 organization 插件,支持组织作为 Stripe 客户。订阅的计费对象由个人用户转为组织,适合 B2B 场景。
启用组织客户时:
- 组织首次订阅时自动创建 Stripe 客户
- 同步组织名称更改到 Stripe 客户
- 有活跃订阅的组织无法被删除
启用组织客户
设置 organization.enabled 为 true,并确保已安装组织插件:
plugins: [
organization(),
stripe({
// ... 其他选项
subscription: {
enabled: true,
plans: [...],
},
organization: {
enabled: true
}
})
]创建组织订阅
即使启用了组织客户,用户订阅仍可用且为默认。若需组织作为计费客户,传入 customerType: "organization":
await authClient.subscription.upgrade({
plan: "team",
referenceId: activeOrg.id,
customerType: "organization",
seats: 10,
successUrl: "/org/billing/success",
cancelUrl: "/org/billing"
});授权
请实现 authorizeReference,确认用户有权管理组织订阅:
subscription: {
// ... 其他订阅选项
authorizeReference: async ({ user, referenceId, action }) => {
const member = await db.members.findFirst({
where: {
userId: user.id,
organizationId: referenceId
}
});
return member?.role === "owner" || member?.role === "admin";
}
}组织计费邮箱
组织没有唯一邮箱,账单邮箱不会自动同步。组织一般使用专门账单邮箱,和用户账户不同。
你可在结账后通过 Stripe 仪表盘修改邮箱,或用 stripeClient 自定义:
await stripeClient.customers.update(organization.stripeCustomerId, {
email: "billing@company.com"
});自定义结账会话参数
你可以传递额外参数给 Stripe Checkout:
getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
return {
params: {
allow_promotion_codes: true,
tax_id_collection: {
enabled: true
},
billing_address_collection: "required",
custom_text: {
submit: {
message: "我们将立即为您启用订阅"
}
},
metadata: {
planType: "business",
referralCode: user.metadata?.referralCode
}
},
options: {
idempotencyKey: `sub_${user.id}_${plan.name}_${Date.now()}`
}
};
}税务收集
开启税务 ID 收集:
subscription: {
// ... 其他选项
getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
return {
params: {
tax_id_collection: {
enabled: true
}
}
};
}
}自动税费计算
启用自动税费计算(按客户位置),设置 automatic_tax:
subscription: {
// ... 其他选项
getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
return {
params: {
automatic_tax: {
enabled: true
}
}
};
}
}注意需先在 Stripe 仪表盘设置税务注册和配置。
试用期管理
插件自动防止用户多次获取免费试用。用户在任一计划使用过试用后,其他计划将不再提供试用。
原理:
- 系统跟踪用户所有计划的试用使用情况
- 用户订阅含试用的计划时,查询历史是否曾试用
- 若有试用记录(
trialStart、trialEnd或trialing状态),则新订阅不提供试用 - 防止用户反复取消订阅、重新订阅以骗取多次试用
示例:
- 用户订阅“Starter”计划,有 7 天试用
- 用户试用后取消订阅
- 用户尝试订阅“Premium”计划,试用被禁止
- 用户即刻支付 Premium 计划费用
此行为自动生效,无需额外配置,且不可通过配置覆盖。
故障排查
webhook 问题
若 webhook 未正确处理:
- 确认 Stripe 仪表盘配置的 webhook URL 是否正确
- 验证 webhook 签名密钥是否正确
- 检查是否勾选全部必需事件
- 查看服务器日志是否有处理错误
订阅状态异常
若订阅状态未正确更新:
- 确认 webhook 事件已接收并处理
- 确认
stripeCustomerId与stripeSubscriptionId是否正确填写 - 核对应用中引用 ID 是否与 Stripe 一致
本地测试 webhook
可用 Stripe CLI 转发 webhook 至本地:
stripe listen --forward-to localhost:3000/api/auth/stripe/webhookCLI 会输出本地使用的 webhook 签名密钥,配置至本地环境即可。