Stripe
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: "2025-02-24.acacia",
})
export const auth = betterAuth({
// ... 您现有的配置
plugins: [
stripe({
stripeClient,
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
createCustomerOnSignUp: true,
})
]
})
添加客户端插件
import { createAuthClient } from "better-auth/client"
import { stripeClient } from "@better-auth/stripe/client"
export const client = createAuthClient({
// ... 您现有的配置
plugins: [
stripeClient({
subscription: true // 如果您想启用订阅管理功能
})
]
})
迁移数据库
运行迁移或生成模式,以将必要的表添加到数据库中。
npx @better-auth/cli migrate
npx @better-auth/cli generate
请参阅 Schema 部分以手动添加表。
设置 Stripe Webhooks
在您的 Stripe 仪表板中创建一个指向以下地址的 Webhook 端点:
https://your-domain.com/api/auth/stripe/webhook
/api/auth
是认证服务器的默认路径。
请确保至少选择以下事件:
checkout.session.completed
customer.subscription.updated
customer.subscription.deleted
保存 Stripe 提供的 Webhook 签名密钥,并将其作为 STRIPE_WEBHOOK_SECRET
添加到您的环境变量中。
使用方法
客户管理
您可以在不启用订阅功能的情况下,仅使用此插件进行客户管理。如果您只想将 Stripe 客户与您的用户关联起来,这将非常有用。
默认情况下,当您设置 createCustomerOnSignUp: true
时,用户注册时会自动创建一个 Stripe 客户。该客户会在您的数据库中与用户关联。
您可以自定义客户创建过程:
stripe({
// ... 其他选项
createCustomerOnSignUp: true,
onCustomerCreate: async ({ customer, stripeCustomer, user }, request) => {
// 对新建的客户执行一些操作
console.log(`为用户 ${user.id} 创建了客户 ${customer.id}`);
},
getCustomerCreateParams: async ({ user, session }, request) => {
// 自定义 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, // 免费试用天数
}
}
]
}
// 动态方案(从数据库或API获取)
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, seats: 1, successUrl, // required cancelUrl, // required returnUrl, disableRedirect: true, // required});
属性 | 描述 | 类型 |
---|---|---|
plan | 要升级到的套餐名称。 | string |
annual? | 是否升级到年度套餐。 | boolean |
referenceId? | 要升级的订阅的参考 ID。 | string |
subscriptionId? | 要升级的订阅的 ID。 | string |
metadata? | Record<string, any> | |
seats? | 要升级到的席位数量(如果适用)。 | number |
successUrl | 订阅成功后重定向的回调 URL。 | string |
cancelUrl | 如果设置,结账页面将显示返回按钮,客户取消付款后将重定向到此 URL。 | string |
returnUrl? | 客户点击账单门户返回网站链接时重定向的 URL。 | string |
disableRedirect | 订阅成功后禁用重定向。 | boolean |
简单示例:
await client.subscription.upgrade({
plan: "pro",
successUrl: "/dashboard",
cancelUrl: "/pricing",
annual: true, // 可选:升级到年度计划
referenceId: "org_123" // 可选:默认为当前登录用户 ID
seats: 5 // 可选:适用于团队计划
});
这将创建一个结账会话并将用户重定向到 Stripe 结账页面。
如果用户已有活跃订阅,您必须提供 subscriptionId
参数。否则,用户将同时订阅(并支付)两个计划。
重要提示:
successUrl
参数将在内部被修改,以处理结账完成和 webhook 处理之间的竞态条件。插件会创建一个中间重定向,确保在重定向到您的成功页面之前,订阅状态已正确更新。
const { error } = await client.subscription.upgrade({
plan: "pro",
successUrl: "/dashboard",
cancelUrl: "/pricing",
});
if(error) {
alert(error.message);
}
对于每个引用 ID(用户或组织),一次只能有一个活跃或试用中的订阅。插件目前不支持同一引用 ID 同时存在多个活跃订阅。
切换计划
要将订阅切换到不同的计划,请使用 subscription.upgrade
方法:
await client.subscription.upgrade({
plan: "pro",
successUrl: "/dashboard",
cancelUrl: "/pricing",
subscriptionId: "sub_123", // 用户当前计划的 Stripe 订阅 ID
});
这确保用户只需为新计划付费,而不是同时支付两个计划的费用。
列出有效订阅
要获取用户的有效订阅:
const { data: subscriptions, error } = await authClient.subscription.list({ referenceId: '123',});// 获取有效订阅const activeSubscription = subscriptions.find( sub => sub.status === "active" || sub.status === "trialing");// 检查订阅限制const projectLimit = subscriptions?.limits?.projects || 0;
属性 | 描述 | 类型 |
---|---|---|
referenceId? | 要列出的订阅的参考ID。 | string |
取消订阅
要取消订阅:
const { data, error } = await authClient.subscription.cancel({ referenceId: 'org_123', subscriptionId: 'sub_123', returnUrl: '/account', // required});
属性 | 描述 | 类型 |
---|---|---|
referenceId? | 要取消的订阅的参考ID。默认为用户ID。 | string |
subscriptionId? | 要取消的订阅的ID。 | string |
returnUrl | 当客户点击账单门户的链接返回您的网站时,将他们带到的URL。 | string |
这将重定向用户到 Stripe 账单门户,他们可以在那里取消订阅。
恢复已取消的订阅
如果用户在取消订阅后改变主意(但在订阅期结束之前),您可以恢复该订阅:
const { data, error } = await authClient.subscription.restore({ referenceId: '123', subscriptionId: 'sub_123',});
属性 | 描述 | 类型 |
---|---|---|
referenceId? | 要恢复的订阅的参考ID。默认为用户ID。 | string |
subscriptionId? | 要恢复的订阅的ID。 | string |
这将重新激活之前设置为在计费周期结束时取消的订阅(cancelAtPeriodEnd: true
)。订阅将继续自动续订。
注意: 这仅适用于仍处于活动状态但标记为在周期结束时取消的订阅。无法恢复已结束的订阅。
创建结算门户会话
要创建一个 Stripe 结算门户会话,让客户可以管理他们的订阅、更新支付方式并查看账单历史:
const { data, error } = await authClient.subscription.billingPortal({ referenceId: "123", returnUrl,});
属性 | 描述 | 类型 |
---|---|---|
referenceId? | 要升级的订阅的参考ID。 | string |
returnUrl? | 订阅成功后重定向返回的URL。 | string |
此端点创建一个 Stripe 结算门户会话,并在响应中返回一个 URL 作为 data.url
。您可以将用户重定向到此 URL,以便他们管理订阅、支付方式和账单历史。
引用系统
默认情况下,订阅与用户 ID 相关联。但您可以使用自定义引用 ID 将订阅与其他实体(如组织)关联:
// 为组织创建订阅
await client.subscription.upgrade({
plan: "pro",
referenceId: "org_123456",
successUrl: "/dashboard",
cancelUrl: "/pricing",
seats: 5 // 团队计划的席位数量
});
// 列出组织的订阅
const { data: subscriptions } = await client.subscription.list({
query: {
referenceId: "org_123456"
}
});
带席位的团队订阅
对于团队或组织计划,您可以指定席位数量:
await client.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.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);
},
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, user }, request) => {
// 试用结束时调用
await sendTrialEndEmail(user.email);
},
onTrialExpired: async (subscription) => {
// 试用过期未转化时调用
await sendTrialExpiredEmail(subscription.referenceId);
}
}
}
数据库结构
Stripe 插件会在您的数据库中创建以下表:
用户表 (User)
表名:user
字段名称 | 类型 | Key | 描述 |
---|---|---|---|
stripeCustomerId | string | Stripe 客户 ID |
订阅
表名:subscription
字段名称 | 类型 | Key | 描述 |
---|---|---|---|
id | string | 每个订阅的唯一标识符 | |
plan | string | - | 订阅计划的名称 |
referenceId | string | - | 此订阅关联的ID(默认为用户ID) |
stripeCustomerId | string | Stripe客户ID | |
stripeSubscriptionId | string | Stripe订阅ID | |
status | string | - | 订阅状态(active-活跃、canceled-已取消等) |
periodStart | Date | 当前计费周期的开始日期 | |
periodEnd | Date | 当前计费周期的结束日期 | |
cancelAtPeriodEnd | boolean | 是否将在周期结束时取消订阅 | |
seats | number | 团队计划的席位数量 | |
trialStart | Date | 试用期的开始日期 | |
trialEnd | Date | 试用期的结束日期 |
自定义模式
要更改模式表名或字段,可以向 Stripe 插件传递 schema
选项:
stripe({
// ... 其他选项
schema: {
subscription: {
modelName: "stripeSubscriptions", // 将订阅表映射到 stripeSubscriptions
fields: {
plan: "planName" // 将 plan 字段映射到 planName
}
}
}
})
选项
主要选项
stripeClient: Stripe
- Stripe 客户端实例。必需。
stripeWebhookSecret: string
- 来自 Stripe 的 webhook 签名密钥。必需。
createCustomerOnSignUp: boolean
- 是否在用户注册时自动创建 Stripe 客户。默认值:false
。
onCustomerCreate: (data: { customer: Customer, stripeCustomer: Stripe.Customer, user: User }, request?: Request) => Promise<void>
- 客户创建后调用的函数。
getCustomerCreateParams: (data: { user: User, session: Session }, request?: Request) => Promise<{}>
- 用于自定义 Stripe 客户创建参数的函数。
onEvent: (event: Stripe.Event) => Promise<void>
- 用于处理任何 Stripe webhook 事件的函数。
订阅选项
enabled: boolean
- 是否启用订阅功能。必需。
plans: Plan[] | (() => Promise<Plan[]>)
- 订阅计划数组或返回计划的函数。如果启用了订阅则为必需。
requireEmailVerification: boolean
- 是否要求在允许订阅升级前进行邮箱验证。默认值:false
。
authorizeReference: (data: { user: User, session: Session, referenceId: string, action: "upgrade-subscription" | "list-subscription" | "cancel-subscription" | "restore-subscription"}, request?: Request) => Promise<boolean>
- 用于授权引用 ID 的函数。
计划配置
每个计划可以具有以下属性:
name:string
- 套餐名称。必填。
priceId:string
- Stripe 价格 ID。除非使用 lookupKey
,否则为必填项。
lookupKey:string
- Stripe 价格查找键。可作为 priceId
的替代选项。
annualDiscountPriceId:string
- 年费计费模式下的价格 ID。
annualDiscountLookupKey:string
- 年费计费模式下的 Stripe 价格查找键。可作为 annualDiscountPriceId
的替代选项。
limits:Record<string, number>
- 与套餐相关的限制(例如 { projects: 10, storage: 5 }
)。
group:string
- 套餐的分组名称,可用于对套餐进行分类。
freeTrial:包含试用配置的对象:
- days:
number
- 试用天数。 - onTrialStart:
(subscription: Subscription) => Promise<void>
- 试用开始时调用。 - onTrialEnd:
(data: { subscription: Subscription, user: User }, request?: Request) => Promise<void>
- 试用结束时调用。 - onTrialExpired:
(subscription: Subscription) => Promise<void>
- 试用到期但未转化时调用。
高级用法
与组织功能配合使用
Stripe 插件与组织插件配合良好。您可以将订阅关联到组织而非个人用户:
// 获取当前活跃组织
const { data: activeOrg } = client.useActiveOrganization();
// 为组织创建订阅
await client.subscription.upgrade({
plan: "team",
referenceId: activeOrg.id,
seats: 10,
annual: true, // 升级为年度计划(可选)
successUrl: "/org/billing/success",
cancelUrl: "/org/billing"
});
请确保实现 authorizeReference
函数来验证用户是否有权限管理组织的订阅:
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 结账会话:
getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
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()}`
}
};
}
税务信息收集
如需向客户收集税号,请将 tax_id_collection
设置为 true:
subscription: {
// ... 其他选项
getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
return {
params: {
tax_id_collection: {
enabled: true
}
}
};
}
}
自动税费计算
如需根据客户所在地自动计算税费,请将 automatic_tax
设置为 true。启用此参数后,Checkout 将收集税费计算所需的账单地址信息。您需要先在 Stripe 控制台中设置并配置税务登记,此功能才能生效。
subscription: {
// ... 其他选项
getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
return {
params: {
automatic_tax: {
enabled: true
}
}
};
}
}
故障排除
Webhook 问题
如果 Webhook 未正确处理:
- 检查您的 Webhook URL 是否已在 Stripe 控制台中正确配置
- 验证 Webhook 签名密钥是否正确
- 确保已在 Stripe 控制台中选择了所有必要的事件
- 检查服务器日志中 Webhook 处理期间是否有任何错误
订阅状态问题
如果订阅状态未正确更新:
- 确保 Webhook 事件已被接收并处理
- 检查
stripeCustomerId
和stripeSubscriptionId
字段是否正确填充 - 验证您的应用程序与 Stripe 之间的参考 ID 是否匹配
本地测试 Webhooks
在本地开发环境中,您可以使用 Stripe CLI 将 webhook 请求转发到本地服务:
stripe listen --forward-to localhost:3000/api/auth/stripe/webhook
此命令将为您提供一个 webhook 签名密钥,您可以在本地环境中使用该密钥进行验证。