Stripe
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-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 migratenpx @better-auth/cli generate请参阅 Schema 部分以手动添加表。
设置 Stripe webhook
在您的 Stripe 仪表板中创建一个 webhook 端点,指向:
https://your-domain.com/api/auth/stripe/webhook/api/auth 是 auth 服务器的默认路径。
确保至少选择这些事件:
checkout.session.completedcustomer.subscription.updatedcustomer.subscription.deleted
保存 Stripe 提供的 webhook 签名密钥,并将其添加到您的环境变量中作为 STRIPE_WEBHOOK_SECRET。
使用
客户管理
您可以仅用于客户管理,而无需启用订阅。这对于仅想将 Stripe 客户链接到您的用户很有用。
默认情况下,当用户注册时,如果设置 createCustomerOnSignUp: true,则会自动创建一个 Stripe 客户。此客户将在您的数据库中链接到用户。
您可以自定义客户创建过程:
stripe({
// ... 其他选项
createCustomerOnSignUp: true,
onCustomerCreate: async ({ customer, stripeCustomer, user }, request) => {
// 对新创建的客户执行某些操作
console.log(`Customer ${customer.id} created for user ${user.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});| Prop | Description | Type |
|---|---|---|
plan | 要升级到的计划名称。 | string |
annual? | 是否升级到年度计划。 | boolean |
referenceId? | 要升级的订阅的引用 ID。 | string |
subscriptionId? | 要升级的订阅 ID。 | string |
metadata? | Record<string, any> | |
seats? | 要升级到的席位数量(如果适用)。 | number |
successUrl | 订阅成功后的回调 URL,用于重定向返回。 | string |
cancelUrl | 如果设置,结账显示后退按钮,如果客户取消支付,将引导到此处。 | string |
returnUrl? | 点击计费门户链接返回网站时引导客户的 URL。 | string |
disableRedirect | 订阅成功后禁用重定向。 | boolean |
简单示例:
await client.subscription.upgrade({
plan: "pro",
successUrl: "/dashboard",
cancelUrl: "/pricing",
annual: true, // 可选:升级到年度计划
referenceId: "org_123", // 可选:默认为当前登录用户 ID
seats: 5 // 可选:用于团队计划
});这将创建一个 Checkout 会话并将用户重定向到 Stripe Checkout 页面。
如果用户已有活跃订阅,您 必须 提供 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;| Prop | Description | Type |
|---|---|---|
referenceId? | 要列出的订阅的引用 ID。 | string |
取消订阅
要取消订阅:
const { data, error } = await authClient.subscription.cancel({ referenceId: 'org_123', subscriptionId: 'sub_123', returnUrl: '/account', // required});| Prop | Description | Type |
|---|---|---|
referenceId? | 要取消的订阅的引用 ID。默认为 userId。 | string |
subscriptionId? | 要取消的订阅 ID。 | string |
returnUrl | 点击计费门户链接返回网站时引导客户的 URL。 | string |
这将用户重定向到 Stripe 计费门户,在那里他们可以取消订阅。
恢复已取消订阅
如果用户在取消订阅后(但在订阅期结束前)改变主意,您可以恢复订阅:
const { data, error } = await authClient.subscription.restore({ referenceId: '123', subscriptionId: 'sub_123',});| Prop | Description | Type |
|---|---|---|
referenceId? | 要恢复的订阅的引用 ID。默认为 userId。 | string |
subscriptionId? | 要恢复的订阅 ID。 | string |
这将重新激活先前设置为在计费期结束时取消的订阅(cancelAtPeriodEnd: true)。订阅将继续自动续订。
注意: 这仅适用于仍活跃但标记为在期结束时取消的订阅。它无法恢复已结束的订阅。
创建计费门户会话
要创建 Stripe 计费门户会话,客户可以在其中管理订阅、更新支付方式并查看计费历史:
const { data, error } = await authClient.subscription.billingPortal({ locale, referenceId: "123", returnUrl,});| Prop | Description | Type |
|---|---|---|
locale? | 客户门户显示的语言标签。如果为空或 auto,则使用浏览器的语言。 | string |
referenceId? | 要升级的订阅的引用 ID。 | string |
returnUrl? | 订阅成功后的返回 URL,用于重定向。 | string |
有关支持的语言,请参阅 IETF 语言标签文档。
此端点创建 Stripe 计费门户会话,并在响应中作为 data.url 返回 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 ${subscription.id} updated`);
},
onSubscriptionCancel: async ({ event, subscription, stripeSubscription, cancellationDetails }) => {
// 订阅取消时调用
await sendCancellationEmail(subscription.referenceId);
},
onSubscriptionDeleted: async ({ event, subscription, stripeSubscription }) => {
// 订阅删除时调用
console.log(`Subscription ${subscription.id} deleted`);
}
}试用期
您可以为计划配置试用期:
{
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);
}
}
}Schema
Stripe 插件会向您的数据库添加以下表:
User
表名:user
| Field Name | Type | Key | Description |
|---|---|---|---|
| stripeCustomerId | string | Stripe 客户 ID |
Subscription
表名:subscription
| Field Name | Type | Key | Description |
|---|---|---|---|
| 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 | 试用期的结束日期 |
自定义 Schema
要更改 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: (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";
}自定义 Checkout 会话参数
您可以使用额外参数自定义 Stripe Checkout 会话:
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: "We'll start your subscription right away"
}
},
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
}
}
};
}
}试用期管理
Stripe 插件自动防止用户获得多个免费试用。一旦用户使用过试用期(无论哪个计划),他们将不再有资格在任何计划上获得额外试用。
工作原理:
- 系统跟踪每个用户的所有计划的试用使用情况
- 当用户订阅带有试用的计划时,系统检查他们的订阅历史
- 如果用户曾经有过试用(由
trialStart/trialEnd字段或trialing状态指示),则不会提供新试用 - 这防止了用户取消订阅并重新订阅以获得多个免费试用的滥用
示例场景:
- 用户订阅 "Starter" 计划,带有 7 天试用
- 用户在试用后取消订阅
- 用户尝试订阅 "Premium" 计划 - 不会提供试用
- 用户将立即为 Premium 计划付费
此行为是自动的,无需额外配置。试用资格在订阅创建时确定,无法通过配置覆盖。
故障排除
Webhook 问题
如果 webhook 未正确处理:
- 检查您的 webhook URL 是否在 Stripe 仪表板中正确配置
- 验证 webhook 签名密钥是否正确
- 确保在 Stripe 仪表板中选择了所有必要事件
- 检查服务器日志中 webhook 处理期间的任何错误
订阅状态问题
如果订阅状态未正确更新:
- 确保 webhook 事件被接收和处理
- 检查
stripeCustomerId和stripeSubscriptionId字段是否正确填充 - 验证应用程序和 Stripe 之间的引用 ID 是否匹配
本地测试 Webhook
对于本地开发,您可以使用 Stripe CLI 将 webhook 转发到本地环境:
stripe listen --forward-to localhost:3000/api/auth/stripe/webhook这将为您提供可在本地环境中使用的 webhook 签名密钥。