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 配置中

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 webhook

在您的 Stripe 仪表板中创建一个 webhook 端点,指向:

https://your-domain.com/api/auth/stripe/webhook

/api/auth 是 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(`Customer ${customer.id} created for user ${user.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});
PropDescriptionType
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

简单示例:

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

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;
PropDescriptionType
referenceId?
要列出的订阅的引用 ID。
string

取消订阅

要取消订阅:

POST
/subscription/cancel
const { data, error } = await authClient.subscription.cancel({    referenceId: 'org_123',    subscriptionId: 'sub_123',    returnUrl: '/account', // required});
PropDescriptionType
referenceId?
要取消的订阅的引用 ID。默认为 userId。
string
subscriptionId?
要取消的订阅 ID。
string
returnUrl
点击计费门户链接返回网站时引导客户的 URL。
string

这将用户重定向到 Stripe 计费门户,在那里他们可以取消订阅。

恢复已取消订阅

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

POST
/subscription/restore
const { data, error } = await authClient.subscription.restore({    referenceId: '123',    subscriptionId: 'sub_123',});
PropDescriptionType
referenceId?
要恢复的订阅的引用 ID。默认为 userId。
string
subscriptionId?
要恢复的订阅 ID。
string

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

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

创建计费门户会话

要创建 Stripe 计费门户会话,客户可以在其中管理订阅、更新支付方式并查看计费历史:

POST
/subscription/billing-portal
const { data, error } = await authClient.subscription.billingPortal({    locale,    referenceId: "123",    returnUrl,});
PropDescriptionType
locale?
客户门户显示的语言标签。如果为空或 auto,则使用浏览器的语言。
string
referenceId?
要升级的订阅的引用 ID。
string
returnUrl?
订阅成功后的返回 URL,用于重定向。
string

有关支持的语言,请参阅 IETF 语言标签文档

此端点创建 Stripe 计费门户会话,并在响应中作为 data.url 返回 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 ${subscription.id} updated`);
    },
    onSubscriptionCancel: async ({ event, subscription, stripeSubscription, cancellationDetails }) => {
        // 订阅取消时调用
        await sendCancellationEmail(subscription.referenceId);
    },
    onSubscriptionDeleted: async ({ event, subscription, stripeSubscription }) => {
        // 订阅删除时调用
        console.log(`Subscription ${subscription.id} deleted`);
    }
}

试用期

您可以为计划配置试用期:

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);
        }
    }
}

Schema

Stripe 插件会向您的数据库添加以下表:

User

表名:user

Field NameTypeKeyDescription
stripeCustomerIdstringStripe 客户 ID

Subscription

表名:subscription

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

自定义 Schema

要更改 schema 表名或字段,您可以将 schema 选项传递给 Stripe 插件:

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

计划配置

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

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 插件与组织插件配合良好。您可以将订阅与组织而非单个用户关联:

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";
}

自定义 Checkout 会话参数

您可以使用额外参数自定义 Stripe Checkout 会话:

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: "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:

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
                }
            }
        };
    }
}

试用期管理

Stripe 插件自动防止用户获得多个免费试用。一旦用户使用过试用期(无论哪个计划),他们将不再有资格在任何计划上获得额外试用。

工作原理:

  • 系统跟踪每个用户的所有计划的试用使用情况
  • 当用户订阅带有试用的计划时,系统检查他们的订阅历史
  • 如果用户曾经有过试用(由 trialStart/trialEnd 字段或 trialing 状态指示),则不会提供新试用
  • 这防止了用户取消订阅并重新订阅以获得多个免费试用的滥用

示例场景:

  1. 用户订阅 "Starter" 计划,带有 7 天试用
  2. 用户在试用后取消订阅
  3. 用户尝试订阅 "Premium" 计划 - 不会提供试用
  4. 用户将立即为 Premium 计划付费

此行为是自动的,无需额外配置。试用资格在订阅创建时确定,无法通过配置覆盖。

故障排除

Webhook 问题

如果 webhook 未正确处理:

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

订阅状态问题

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

  1. 确保 webhook 事件被接收和处理
  2. 检查 stripeCustomerIdstripeSubscriptionId 字段是否正确填充
  3. 验证应用程序和 Stripe 之间的引用 ID 是否匹配

本地测试 Webhook

对于本地开发,您可以使用 Stripe CLI 将 webhook 转发到本地环境:

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

这将为您提供可在本地环境中使用的 webhook 签名密钥。