Stripe

Stripe 插件将 Stripe 的支付和订阅功能与 Better Auth 集成。由于支付和身份验证通常紧密相关,该插件简化了 Stripe 在应用中的集成,处理客户创建、订阅管理和 Webhook 处理。

功能特性

  • 用户注册时自动创建 Stripe 客户
  • 管理订阅计划和定价
  • 处理订阅生命周期事件(创建、更新、取消)
  • 通过签名验证安全处理 Stripe Webhook
  • 向应用公开订阅数据
  • 支持试用期和订阅升级
  • 灵活的关联系统,将订阅与用户或组织关联
  • 支持团队订阅及席位管理

安装

安装插件

首先,安装插件:

npm install @better-auth/stripe

如果您使用的是分离的客户端和服务器设置,请确保在项目的两个部分都安装此插件。

安装 Stripe SDK

接下来,在您的服务器上安装 Stripe SDK:

npm install stripe@^18.0.0

将插件添加到您的认证配置中

auth.ts
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,
        })
    ]
})

添加客户端插件

auth-client.ts
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 客户。该客户会在您的数据库中与用户关联。 您可以自定义客户创建过程:

auth.ts
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
            }
        };
    }
})

订阅管理

定义订阅方案

您可以通过静态或动态方式定义订阅方案:

auth.ts
// 静态方案
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 方法:

POST
/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

简单示例:

client.ts
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 方法:

client.ts
await client.subscription.upgrade({
    plan: "pro",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    subscriptionId: "sub_123", // 用户当前计划的 Stripe 订阅 ID
});

这确保用户只需为新计划付费,而不是同时支付两个计划的费用。

列出有效订阅

要获取用户的有效订阅:

GET
/subscription/list
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

取消订阅

要取消订阅:

POST
/subscription/cancel
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 账单门户,他们可以在那里取消订阅。

恢复已取消的订阅

如果用户在取消订阅后改变主意(但在订阅期结束之前),您可以恢复该订阅:

POST
/subscription/restore
const { data, error } = await authClient.subscription.restore({    referenceId: '123',    subscriptionId: 'sub_123',});
属性描述类型
referenceId?
要恢复的订阅的参考ID。默认为用户ID。
string
subscriptionId?
要恢复的订阅的ID。
string

这将重新激活之前设置为在计费周期结束时取消的订阅(cancelAtPeriodEnd: true)。订阅将继续自动续订。

注意: 这仅适用于仍处于活动状态但标记为在周期结束时取消的订阅。无法恢复已结束的订阅。

创建结算门户会话

要创建一个 Stripe 结算门户会话,让客户可以管理他们的订阅、更新支付方式并查看账单历史:

POST
/subscription/billing-portal
const { data, error } = await authClient.subscription.billingPortal({    referenceId: "123",    returnUrl,});
属性描述类型
referenceId?
要升级的订阅的参考ID。
string
returnUrl?
订阅成功后重定向返回的URL。
string

此端点创建一个 Stripe 结算门户会话,并在响应中返回一个 URL 作为 data.url。您可以将用户重定向到此 URL,以便他们管理订阅、支付方式和账单历史。

引用系统

默认情况下,订阅与用户 ID 相关联。但您可以使用自定义引用 ID 将订阅与其他实体(如组织)关联:

client.ts
// 为组织创建订阅
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 函数:

auth.ts
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:将订阅标记为已取消

您也可以处理自定义事件:

auth.ts
stripe({
    // ... 其他选项
    onEvent: async (event) => {
        // 处理任意 Stripe 事件
        switch (event.type) {
            case "invoice.paid":
                // 处理已支付发票
                break;
            case "payment_intent.succeeded":
                // 处理支付成功
                break;
        }
    }
})

订阅生命周期钩子

您可以挂钩到各种订阅生命周期事件中:

auth.ts
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} 已删除`);
    }
}

试用期设置

您可以为套餐配置试用期:

auth.ts
{
    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描述
stripeCustomerIdstringStripe 客户 ID

订阅

表名:subscription

字段名称类型Key描述
idstring每个订阅的唯一标识符
planstring-订阅计划的名称
referenceIdstring-此订阅关联的ID(默认为用户ID)
stripeCustomerIdstringStripe客户ID
stripeSubscriptionIdstringStripe订阅ID
statusstring-订阅状态(active-活跃、canceled-已取消等)
periodStartDate当前计费周期的开始日期
periodEndDate当前计费周期的结束日期
cancelAtPeriodEndboolean是否将在周期结束时取消订阅
seatsnumber团队计划的席位数量
trialStartDate试用期的开始日期
trialEndDate试用期的结束日期

自定义模式

要更改模式表名或字段,可以向 Stripe 插件传递 schema 选项:

auth.ts
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 的函数。

计划配置

每个计划可以具有以下属性:

namestring - 套餐名称。必填。

priceIdstring - Stripe 价格 ID。除非使用 lookupKey,否则为必填项。

lookupKeystring - Stripe 价格查找键。可作为 priceId 的替代选项。

annualDiscountPriceIdstring - 年费计费模式下的价格 ID。

annualDiscountLookupKeystring - 年费计费模式下的 Stripe 价格查找键。可作为 annualDiscountPriceId 的替代选项。

limitsRecord<string, number> - 与套餐相关的限制(例如 { projects: 10, storage: 5 })。

groupstring - 套餐的分组名称,可用于对套餐进行分类。

freeTrial:包含试用配置的对象:

  • daysnumber - 试用天数。
  • onTrialStart(subscription: Subscription) => Promise<void> - 试用开始时调用。
  • onTrialEnd(data: { subscription: Subscription, user: User }, request?: Request) => Promise<void> - 试用结束时调用。
  • onTrialExpired(subscription: Subscription) => Promise<void> - 试用到期但未转化时调用。

高级用法

与组织功能配合使用

Stripe 插件与组织插件配合良好。您可以将订阅关联到组织而非个人用户:

client.ts
// 获取当前活跃组织
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 函数来验证用户是否有权限管理组织的订阅:

auth.ts
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 结账会话:

auth.ts
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:

auth.ts
subscription: {
    // ... 其他选项
    getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
        return {
            params: {
                tax_id_collection: {
                    enabled: true
                }
            }
        };
    }
}

自动税费计算

如需根据客户所在地自动计算税费,请将 automatic_tax 设置为 true。启用此参数后,Checkout 将收集税费计算所需的账单地址信息。您需要先在 Stripe 控制台中设置并配置税务登记,此功能才能生效。

auth.ts
subscription: {
    // ... 其他选项
    getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
        return {
            params: {
                automatic_tax: {
                    enabled: true
                }
            }
        };
    }
}

故障排除

Webhook 问题

如果 Webhook 未正确处理:

  1. 检查您的 Webhook URL 是否已在 Stripe 控制台中正确配置
  2. 验证 Webhook 签名密钥是否正确
  3. 确保已在 Stripe 控制台中选择了所有必要的事件
  4. 检查服务器日志中 Webhook 处理期间是否有任何错误

订阅状态问题

如果订阅状态未正确更新:

  1. 确保 Webhook 事件已被接收并处理
  2. 检查 stripeCustomerIdstripeSubscriptionId 字段是否正确填充
  3. 验证您的应用程序与 Stripe 之间的参考 ID 是否匹配

本地测试 Webhooks

在本地开发环境中,您可以使用 Stripe CLI 将 webhook 请求转发到本地服务:

stripe listen --forward-to localhost:3000/api/auth/stripe/webhook

此命令将为您提供一个 webhook 签名密钥,您可以在本地环境中使用该密钥进行验证。