Stripe
Stripe 插件,用于 Better Auth,管理订阅和支付。
Stripe 插件将 Stripe 的支付与订阅功能集成到 Better Auth 中。由于支付和认证通常紧密相关,此插件简化了在应用中集成 Stripe 的流程,负责客户创建、订阅管理和 webhook 处理。
功能
- 创建用户注册时的 Stripe 客户
- 管理订阅计划和定价
- 处理订阅生命周期事件(创建、更新、取消)
- 安全处理 Stripe webhook 并进行签名验证
- 向应用暴露订阅数据
- 支持试用期和订阅升级
- 自动试用期滥用防护 - 每个账户在所有计划中只能获得一次试用
- 灵活的引用系统,用于关联订阅与用户或组织
- 支持带席位管理的团队订阅
安装
将插件添加到认证配置
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: "2026-03-25.dahlia", // Stripe SDK v22.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升级到的席位数量(如果适用)。
localestringCheckout 显示的语言环境 IETF 标签。 如果未提供或设置为 "auto",则使用浏览器语言环境。
successUrlstringrequiredStripe 完成支付或设置后要跳转到的 URL。
cancelUrlstringrequired如果设置,将显示返回按钮,用户取消支付时将跳转至此。
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(用户或组织)一次仅支持一个活跃或试用中的订阅。同一引用 ID 下不允许同时存在多个订阅。
如果用户已有活跃订阅,则 必须 在升级时提供 subscriptionId 参数。否则,可能会在与现有订阅并行的位置创建新订阅,导致重复计费。
重要提示:
successUrl参数将在内部被修改,以处理结账完成与 webhook 处理之间的竞争条件。插件会创建一个跳转中转页面,在订阅状态正确更新后才跳转到您指定的成功页。
const { error } = await authClient.subscription.upgrade({
plan: "pro",
successUrl: "/dashboard",
cancelUrl: "/pricing",
});
if(error) {
alert(error.message);
}切换计划
要切换订阅计划,可以调用 subscription.upgrade:
要将订阅切换到不同的计划,请使用 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 | 如果订阅已被取消,这是取消的时间 |
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 返回。您可以重定向用户到此地址,让他们管理订阅、支付方式和账单历史记录。
该接口会创建 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, stripeSubscription }) => {
// 订阅更新时调用。使用 `stripeSubscription` 获取原始 Stripe 字段,例如 `cancellation_details`。
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);
}
}
}架构
该 Stripe 插件会向您的数据库添加以下数据表:
用户
表名:user
组织
表名:organization (仅当 organization.enabled 为 true 时有效)
订阅
表名: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。 |
订阅选项
| Option | Type | Description |
|---|---|---|
enabled | boolean | 是否启用订阅功能。必需。 |
plans | StripePlan[] or function | 订阅计划数组,或返回计划的异步函数。启用时必需。 |
requireEmailVerification | boolean | 在允许订阅升级前是否要求邮箱验证。默认:false。 |
authorizeReference | function | 授权 reference ID。接收 { user, session, referenceId, action } 以及上下文。 |
getCheckoutSessionParams | function | 自定义 Stripe Checkout 会话参数。接收 { user, session, plan, subscription }、请求和上下文。 |
onSubscriptionComplete | function | 通过 checkout 创建订阅时调用。接收 { event, stripeSubscription, subscription, plan } 和上下文。 |
onSubscriptionCreated | function | 在 checkout 之外创建订阅时调用。接收 { event, stripeSubscription, subscription, plan }。 |
onSubscriptionUpdate | function | 订阅更新时调用。接收 { event, subscription, stripeSubscription }。像 cancellation_details 这类原始 Stripe 字段请使用 stripeSubscription。 |
onSubscriptionCancel | function | 订阅取消时调用。接收 { event, subscription, stripeSubscription, cancellationDetails }。 |
onSubscriptionDeleted | function | 订阅删除时调用。接收 { event, stripeSubscription, subscription }。 |
计划配置
| Option | Type | Description |
|---|---|---|
name | string | 计划名称。必需。 |
priceId | string | Stripe price ID。除非使用 lookupKey,否则为必需。 |
lookupKey | string | Stripe price lookup key。可替代 priceId。 |
annualDiscountPriceId | string | 年付账单的 price ID。 |
annualDiscountLookupKey | string | 年付账单的 Stripe price lookup key。 |
limits | object | 计划限制(例如 { projects: 10, storage: 5 })。 |
group | string | 用于对计划分组的组名。 |
seatPriceId | string | 按席位计费的 price ID。需要 organization 插件。 |
prorationBehavior | string | 订阅更新时的按比例计费行为:"create_prorations"(默认)、"always_invoice" 或 "none"。 |
lineItems | LineItem[] | 在 checkout 会话中包含的附加条目。 |
freeTrial | object | 试用配置。见下文。 |
Stripe 不支持通过 Checkout Sessions 进行 混合计费周期订阅。checkout 中的所有条目都应使用相同的计费周期(例如全部按月或全部按年)。如果周期不同,Stripe API 会拒绝该请求。
免费试用配置
| Option | Type | Description |
|---|---|---|
days | number | 试用天数。必需。 |
onTrialStart | function | 试用开始时调用。接收 subscription。 |
onTrialEnd | function | 试用结束时调用。接收 { subscription } 和上下文。 |
onTrialExpired | function | 试用在未转化时过期调用。接收 subscription 和上下文。 |
组织选项
| Option | Type | Description |
|---|---|---|
enabled | boolean | 启用组织客户支持。必需。 |
getCustomerCreateParams | function | 自定义组织的 Stripe 客户创建参数。接收 organization 和上下文。 |
onCustomerCreate | function | 组织客户创建后调用。接收 { stripeCustomer, organization } 和上下文。 |
高级用法
与组织插件一起使用
Stripe 插件集成了 organization 插件,支持组织作为 Stripe 客户。订阅的计费对象由个人用户转为组织,适合 B2B 场景。
启用组织客户时:
- 当组织首次订阅时会自动创建 Stripe Customer
- 组织名称变更会同步到 Stripe Customer
- 有活跃订阅的组织无法被删除
启用组织客户
设置 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"
});处理用户删除
有活跃订阅的组织会被自动阻止删除,但用户不会。若要在用户删除时实现相同行为,可在订阅活跃时于 beforeDelete 回调中抛出错误:
import { betterAuth } from "better-auth";
import { APIError } from "better-auth/api";
export const auth = betterAuth({
user: {
deleteUser: {
enabled: true,
beforeDelete: async (user) => {
if (!user.stripeCustomerId) return;
for await (const sub of stripeClient.subscriptions.list({
customer: user.stripeCustomerId,
status: "all",
})) {
if (["canceled", "incomplete", "incomplete_expired"].includes(sub.status)) continue;
throw new APIError("BAD_REQUEST", {
message: "请在删除账户前取消当前活跃订阅",
});
// 或立即取消:await stripeClient.subscriptions.cancel(sub.id);
// 或在周期结束时取消: await stripeClient.subscriptions.update(sub.id, { cancel_at_period_end: true });
}
},
},
},
});自定义 Checkout Session 参数
你可以传递额外参数给 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 计划费用
示例:
- 用户订阅“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 签名密钥,将其配置至本地环境即可。