mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-08 04:19:25 +00:00
* Added `annual: true` on the Create Subscription on Stripe Docs * docs: clarify comment for annual plan upgrade in Stripe integration
683 lines
20 KiB
Plaintext
683 lines
20 KiB
Plaintext
---
|
|
title: Stripe
|
|
description: Stripe plugin for Better Auth to manage subscriptions and payments.
|
|
---
|
|
|
|
The Stripe plugin integrates Stripe's payment and subscription functionality with Better Auth. Since payment and authentication are often tightly coupled, this plugin simplifies the integration of stripe into your application, handling customer creation, subscription management, and webhook processing.
|
|
|
|
<Callout type="warn">
|
|
This plugin is currently in beta. We're actively collecting feedback and exploring additional features. If you have feature requests or suggestions, please join our [Discord community](https://discord.com/invite/Mh3DaacaFs) to discuss them.
|
|
</Callout>
|
|
|
|
## Features
|
|
|
|
- Create Stripe Customers automatically when users sign up
|
|
- Manage subscription plans and pricing
|
|
- Process subscription lifecycle events (creation, updates, cancellations)
|
|
- Handle Stripe webhooks securely with signature verification
|
|
- Expose subscription data to your application
|
|
- Support for trial periods and subscription upgrades
|
|
- Flexible reference system to associate subscriptions with users or organizations
|
|
- Team subscription support with seats management
|
|
|
|
## Installation
|
|
|
|
<Steps>
|
|
<Step>
|
|
### Install the plugin
|
|
|
|
First, install the plugin:
|
|
|
|
```package-install
|
|
@better-auth/stripe
|
|
```
|
|
<Callout>
|
|
If you're using a separate client and server setup, make sure to install the plugin in both parts of your project.
|
|
</Callout>
|
|
</Step>
|
|
<Step>
|
|
### Install the Stripe SDK
|
|
|
|
Next, install the Stripe SDK on your server:
|
|
|
|
```package-install
|
|
stripe
|
|
```
|
|
</Step>
|
|
<Step>
|
|
### Add the plugin to your auth config
|
|
|
|
```ts title="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!)
|
|
|
|
export const auth = betterAuth({
|
|
// ... your existing config
|
|
plugins: [
|
|
stripe({
|
|
stripeClient,
|
|
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
|
|
createCustomerOnSignUp: true,
|
|
})
|
|
]
|
|
})
|
|
```
|
|
</Step>
|
|
<Step>
|
|
### Add the client plugin
|
|
|
|
```ts title="auth-client.ts"
|
|
import { createAuthClient } from "better-auth/client"
|
|
import { stripeClient } from "@better-auth/stripe/client"
|
|
|
|
export const client = createAuthClient({
|
|
// ... your existing config
|
|
plugins: [
|
|
stripeClient({
|
|
subscription: true //if you want to enable subscription management
|
|
})
|
|
]
|
|
})
|
|
```
|
|
</Step>
|
|
<Step>
|
|
### Migrate the database
|
|
|
|
Run the migration or generate the schema to add the necessary tables to the database.
|
|
|
|
<Tabs items={["migrate", "generate"]}>
|
|
<Tab value="migrate">
|
|
```bash
|
|
npx @better-auth/cli migrate
|
|
```
|
|
</Tab>
|
|
<Tab value="generate">
|
|
```bash
|
|
npx @better-auth/cli generate
|
|
```
|
|
</Tab>
|
|
</Tabs>
|
|
See the [Schema](#schema) section to add the tables manually.
|
|
</Step>
|
|
<Step>
|
|
### Set up Stripe webhooks
|
|
|
|
Create a webhook endpoint in your Stripe dashboard pointing to:
|
|
|
|
```
|
|
https://your-domain.com/api/auth/stripe/webhook
|
|
```
|
|
`/api/auth` is the default path for the auth server.
|
|
|
|
Make sure to select at least these events:
|
|
- `checkout.session.completed`
|
|
- `customer.subscription.updated`
|
|
- `customer.subscription.deleted`
|
|
|
|
Save the webhook signing secret provided by Stripe and add it to your environment variables as `STRIPE_WEBHOOK_SECRET`.
|
|
</Step>
|
|
</Steps>
|
|
|
|
## Usage
|
|
|
|
### Customer Management
|
|
|
|
You can use this plugin solely for customer management without enabling subscriptions. This is useful if you just want to link Stripe customers to your users.
|
|
|
|
By default, when a user signs up, a Stripe customer is automatically created if you set `createCustomerOnSignUp: true`. This customer is linked to the user in your database.
|
|
You can customize the customer creation process:
|
|
|
|
```ts title="auth.ts"
|
|
stripe({
|
|
// ... other options
|
|
createCustomerOnSignUp: true,
|
|
onCustomerCreate: async ({ customer, stripeCustomer, user }, request) => {
|
|
// Do something with the newly created customer
|
|
console.log(`Customer ${customer.id} created for user ${user.id}`);
|
|
},
|
|
getCustomerCreateParams: async ({ user, session }, request) => {
|
|
// Customize the Stripe customer creation parameters
|
|
return {
|
|
metadata: {
|
|
referralSource: user.metadata?.referralSource
|
|
}
|
|
};
|
|
}
|
|
})
|
|
```
|
|
|
|
### Subscription Management
|
|
|
|
#### Defining Plans
|
|
|
|
You can define your subscription plans either statically or dynamically:
|
|
|
|
```ts title="auth.ts"
|
|
// Static plans
|
|
subscription: {
|
|
enabled: true,
|
|
plans: [
|
|
{
|
|
name: "basic", // the name of the plan, it'll be automatically lower cased when stored in the database
|
|
priceId: "price_1234567890", // the price id from stripe
|
|
annualDiscountPriceId: "price_1234567890", // (optional) the price id for annual billing with a discount
|
|
limits: {
|
|
projects: 5,
|
|
storage: 10
|
|
}
|
|
},
|
|
{
|
|
name: "pro",
|
|
priceId: "price_0987654321",
|
|
limits: {
|
|
projects: 20,
|
|
storage: 50
|
|
},
|
|
freeTrial: {
|
|
days: 14,
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
// Dynamic plans (fetched from database or 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)
|
|
}));
|
|
}
|
|
}
|
|
```
|
|
|
|
see [plan configuration](#plan-configuration) for more.
|
|
|
|
#### Creating a Subscription
|
|
|
|
To create a subscription, use the `subscription.upgrade` method:
|
|
|
|
```ts title="client.ts"
|
|
await client.subscription.upgrade({
|
|
plan: "pro",
|
|
successUrl: "/dashboard",
|
|
cancelUrl: "/pricing",
|
|
annual: true, // Optional: upgrade to an annual plan
|
|
referenceId: "org_123" // Optional: defaults to the current logged in user id
|
|
seats: 5 // Optional: for team plans
|
|
});
|
|
```
|
|
|
|
This will create a Checkout Session and redirect the user to the Stripe Checkout page.
|
|
|
|
> **Important:** The `successUrl` parameter will be internally modified to handle race conditions between checkout completion and webhook processing. The plugin creates an intermediate redirect that ensures subscription status is properly updated before redirecting to your success page.
|
|
|
|
```ts
|
|
const { error } = await client.subscription.upgrade({
|
|
plan: "pro",
|
|
successUrl: "/dashboard",
|
|
cancelUrl: "/pricing",
|
|
});
|
|
if(error) {
|
|
alert(error.message);
|
|
}
|
|
```
|
|
|
|
<Callout type="warn">
|
|
For each reference ID (user or organization), only one active or trialing subscription is supported at a time. The plugin doesn't currently support multiple concurrent active subscriptions for the same reference ID.
|
|
</Callout>
|
|
|
|
|
|
#### Listing Active Subscriptions
|
|
|
|
To get the user's active subscriptions:
|
|
|
|
```ts title="client.ts"
|
|
const { data: subscriptions } = await client.subscription.list();
|
|
|
|
// get the active subscription
|
|
const activeSubscription = subscriptions.find(
|
|
sub => sub.status === "active" || sub.status === "trialing"
|
|
);
|
|
|
|
// Check subscription limits
|
|
const projectLimit = subscriptions?.limits?.projects || 0;
|
|
```
|
|
|
|
#### Canceling a Subscription
|
|
|
|
To cancel a subscription:
|
|
|
|
```ts title="client.ts"
|
|
const { data } = await client.subscription.cancel({
|
|
returnUrl: "/account",
|
|
referenceId: "org_123" // optional defaults to userId
|
|
});
|
|
```
|
|
|
|
This will redirect the user to the Stripe Billing Portal where they can cancel their subscription.
|
|
|
|
### Reference System
|
|
|
|
By default, subscriptions are associated with the user ID. However, you can use a custom reference ID to associate subscriptions with other entities, such as organizations:
|
|
|
|
```ts title="client.ts"
|
|
// Create a subscription for an organization
|
|
await client.subscription.upgrade({
|
|
plan: "pro",
|
|
referenceId: "org_123456",
|
|
successUrl: "/dashboard",
|
|
cancelUrl: "/pricing",
|
|
seats: 5 // Number of seats for team plans
|
|
});
|
|
|
|
// List subscriptions for an organization
|
|
const { data: subscriptions } = await client.subscription.list({
|
|
query: {
|
|
referenceId: "org_123456"
|
|
}
|
|
});
|
|
```
|
|
|
|
#### Team Subscriptions with Seats
|
|
|
|
For team or organization plans, you can specify the number of seats:
|
|
|
|
```ts
|
|
await client.subscription.upgrade({
|
|
plan: "team",
|
|
referenceId: "org_123456",
|
|
seats: 10, // 10 team members
|
|
successUrl: "/org/billing/success",
|
|
cancelUrl: "/org/billing"
|
|
});
|
|
```
|
|
|
|
The `seats` parameter is passed to Stripe as the quantity for the subscription item. You can use this value in your application logic to limit the number of members in a team or organization.
|
|
|
|
To authorize reference IDs, implement the `authorizeReference` function:
|
|
|
|
```ts title="auth.ts"
|
|
subscription: {
|
|
// ... other options
|
|
authorizeReference: async ({ user, session, referenceId, action }) => {
|
|
// Check if the user has permission to manage subscriptions for this reference
|
|
if (action === "upgrade-subscription" || action === "cancel-subscription") {
|
|
const org = await db.member.findFirst({
|
|
where: {
|
|
organizationId: referenceId,
|
|
userId: user.id
|
|
}
|
|
});
|
|
return org?.role === "owner"
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Webhook Handling
|
|
|
|
The plugin automatically handles common webhook events:
|
|
|
|
- `checkout.session.completed`: Updates subscription status after checkout
|
|
- `customer.subscription.updated`: Updates subscription details when changed
|
|
- `customer.subscription.deleted`: Marks subscription as canceled
|
|
|
|
You can also handle custom events:
|
|
|
|
```ts title="auth.ts"
|
|
stripe({
|
|
// ... other options
|
|
onEvent: async (event) => {
|
|
// Handle any Stripe event
|
|
switch (event.type) {
|
|
case "invoice.paid":
|
|
// Handle paid invoice
|
|
break;
|
|
case "payment_intent.succeeded":
|
|
// Handle successful payment
|
|
break;
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
### Subscription Lifecycle Hooks
|
|
|
|
You can hook into various subscription lifecycle events:
|
|
|
|
```ts title="auth.ts"
|
|
subscription: {
|
|
// ... other options
|
|
onSubscriptionComplete: async ({ event, subscription, stripeSubscription, plan }) => {
|
|
// Called when a subscription is successfully created
|
|
await sendWelcomeEmail(subscription.referenceId, plan.name);
|
|
},
|
|
onSubscriptionUpdate: async ({ event, subscription }) => {
|
|
// Called when a subscription is updated
|
|
console.log(`Subscription ${subscription.id} updated`);
|
|
},
|
|
onSubscriptionCancel: async ({ event, subscription, stripeSubscription, cancellationDetails }) => {
|
|
// Called when a subscription is canceled
|
|
await sendCancellationEmail(subscription.referenceId);
|
|
},
|
|
onSubscriptionDeleted: async ({ event, subscription, stripeSubscription }) => {
|
|
// Called when a subscription is deleted
|
|
console.log(`Subscription ${subscription.id} deleted`);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Trial Periods
|
|
|
|
You can configure trial periods for your plans:
|
|
|
|
```ts title="auth.ts"
|
|
{
|
|
name: "pro",
|
|
priceId: "price_0987654321",
|
|
freeTrial: {
|
|
days: 14,
|
|
onTrialStart: async (subscription) => {
|
|
// Called when a trial starts
|
|
await sendTrialStartEmail(subscription.referenceId);
|
|
},
|
|
onTrialEnd: async ({ subscription, user }, request) => {
|
|
// Called when a trial ends
|
|
await sendTrialEndEmail(user.email);
|
|
},
|
|
onTrialExpired: async (subscription) => {
|
|
// Called when a trial expires without conversion
|
|
await sendTrialExpiredEmail(subscription.referenceId);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Schema
|
|
|
|
The Stripe plugin adds the following tables to your database:
|
|
|
|
|
|
### User
|
|
|
|
Table Name: `user`
|
|
|
|
<DatabaseTable
|
|
fields={[
|
|
{
|
|
name: "stripeCustomerId",
|
|
type: "string",
|
|
description: "The Stripe customer ID"
|
|
},
|
|
]}
|
|
/>
|
|
|
|
### Subscription
|
|
|
|
Table Name: `subscription`
|
|
|
|
<DatabaseTable
|
|
fields={[
|
|
{
|
|
name: "id",
|
|
type: "string",
|
|
description: "Unique identifier for each subscription",
|
|
isPrimaryKey: true
|
|
},
|
|
{
|
|
name: "plan",
|
|
type: "string",
|
|
description: "The name of the subscription plan"
|
|
},
|
|
{
|
|
name: "referenceId",
|
|
type: "string",
|
|
description: "The ID this subscription is associated with (user ID by default)",
|
|
isUnique: true
|
|
},
|
|
{
|
|
name: "stripeCustomerId",
|
|
type: "string",
|
|
description: "The Stripe customer ID",
|
|
isOptional: true
|
|
},
|
|
{
|
|
name: "stripeSubscriptionId",
|
|
type: "string",
|
|
description: "The Stripe subscription ID",
|
|
isOptional: true
|
|
},
|
|
{
|
|
name: "status",
|
|
type: "string",
|
|
description: "The status of the subscription (active, canceled, etc.)",
|
|
defaultValue: "incomplete"
|
|
},
|
|
{
|
|
name: "periodStart",
|
|
type: "Date",
|
|
description: "Start date of the current billing period",
|
|
isOptional: true
|
|
},
|
|
{
|
|
name: "periodEnd",
|
|
type: "Date",
|
|
description: "End date of the current billing period",
|
|
isOptional: true
|
|
},
|
|
{
|
|
name: "cancelAtPeriodEnd",
|
|
type: "boolean",
|
|
description: "Whether the subscription will be canceled at the end of the period",
|
|
defaultValue: false,
|
|
isOptional: true
|
|
},
|
|
{
|
|
name: "seats",
|
|
type: "number",
|
|
description: "Number of seats for team plans",
|
|
isOptional: true
|
|
},
|
|
{
|
|
name: "trialStart",
|
|
type: "Date",
|
|
description: "Start date of the trial period",
|
|
isOptional: true
|
|
},
|
|
{
|
|
name: "trialEnd",
|
|
type: "Date",
|
|
description: "End date of the trial period",
|
|
isOptional: true
|
|
}
|
|
]}
|
|
/>
|
|
|
|
### Customizing the Schema
|
|
|
|
To change the schema table names or fields, you can pass a `schema` option to the Stripe plugin:
|
|
|
|
```ts title="auth.ts"
|
|
stripe({
|
|
// ... other options
|
|
schema: {
|
|
subscription: {
|
|
modelName: "stripeSubscriptions", // map the subscription table to stripeSubscriptions
|
|
fields: {
|
|
plan: "planName" // map the plan field to planName
|
|
}
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
## Options
|
|
|
|
### Main Options
|
|
|
|
**stripeClient**: `Stripe` - The Stripe client instance. Required.
|
|
|
|
**stripeWebhookSecret**: `string` - The webhook signing secret from Stripe. Required.
|
|
|
|
**createCustomerOnSignUp**: `boolean` - Whether to automatically create a Stripe customer when a user signs up. Default: `false`.
|
|
|
|
**onCustomerCreate**: `(data: { customer: Customer, stripeCustomer: Stripe.Customer, user: User }, request?: Request) => Promise<void>` - A function called after a customer is created.
|
|
|
|
**getCustomerCreateParams**: `(data: { user: User, session: Session }, request?: Request) => Promise<{}>` - A function to customize the Stripe customer creation parameters.
|
|
|
|
**onEvent**: `(event: Stripe.Event) => Promise<void>` - A function called for any Stripe webhook event.
|
|
|
|
### Subscription Options
|
|
|
|
**enabled**: `boolean` - Whether to enable subscription functionality. Required.
|
|
|
|
**plans**: `Plan[] | (() => Promise<Plan[]>)` - An array of subscription plans or a function that returns plans. Required if subscriptions are enabled.
|
|
|
|
**requireEmailVerification**: `boolean` - Whether to require email verification before allowing subscription upgrades. Default: `false`.
|
|
|
|
**authorizeReference**: `(data: { user: User, session: Session, referenceId: string, action: "upgrade-subscription" | "list-subscription" | "cancel-subscription" }, request?: Request) => Promise<boolean>` - A function to authorize reference IDs.
|
|
|
|
### Plan Configuration
|
|
|
|
Each plan can have the following properties:
|
|
|
|
**name**: `string` - The name of the plan. Required.
|
|
|
|
**priceId**: `string` - The Stripe price ID. Required unless using `lookupKey`.
|
|
|
|
**lookupKey**: `string` - The Stripe price lookup key. Alternative to `priceId`.
|
|
|
|
**annualDiscountPriceId**: `string` - A price ID for annual billing with a discount.
|
|
|
|
**limits**: `Record<string, number>` - Limits associated with the plan (e.g., `{ projects: 10, storage: 5 }`).
|
|
|
|
**group**: `string` - A group name for the plan, useful for categorizing plans.
|
|
|
|
**freeTrial**: Object containing trial configuration:
|
|
- **days**: `number` - Number of trial days.
|
|
- **onTrialStart**: `(subscription: Subscription) => Promise<void>` - Called when a trial starts.
|
|
- **onTrialEnd**: `(data: { subscription: Subscription, user: User }, request?: Request) => Promise<void>` - Called when a trial ends.
|
|
- **onTrialExpired**: `(subscription: Subscription) => Promise<void>` - Called when a trial expires without conversion.
|
|
|
|
## Advanced Usage
|
|
|
|
### Using with Organizations
|
|
|
|
The Stripe plugin works well with the organization plugin. You can associate subscriptions with organizations instead of individual users:
|
|
|
|
```ts title="client.ts"
|
|
// Get the active organization
|
|
const { data: activeOrg } = client.useActiveOrganization();
|
|
|
|
// Create a subscription for the organization
|
|
await client.subscription.upgrade({
|
|
plan: "team",
|
|
referenceId: activeOrg.id,
|
|
seats: 10,
|
|
annual: true, // upgrade to an annual plan (optional)
|
|
successUrl: "/org/billing/success",
|
|
cancelUrl: "/org/billing"
|
|
});
|
|
```
|
|
|
|
Make sure to implement the `authorizeReference` function to verify that the user has permission to manage subscriptions for the organization:
|
|
|
|
```ts title="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";
|
|
}
|
|
```
|
|
|
|
### Custom Checkout Session Parameters
|
|
|
|
You can customize the Stripe Checkout session with additional parameters:
|
|
|
|
```ts title="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 Collection
|
|
|
|
To enable tax collection:
|
|
|
|
```ts title="auth.ts"
|
|
subscription: {
|
|
// ... other options
|
|
getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
|
|
return {
|
|
params: {
|
|
tax_id_collection: {
|
|
enabled: true
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Webhook Issues
|
|
|
|
If webhooks aren't being processed correctly:
|
|
|
|
1. Check that your webhook URL is correctly configured in the Stripe dashboard
|
|
2. Verify that the webhook signing secret is correct
|
|
3. Ensure you've selected all the necessary events in the Stripe dashboard
|
|
4. Check your server logs for any errors during webhook processing
|
|
|
|
### Subscription Status Issues
|
|
|
|
If subscription statuses aren't updating correctly:
|
|
|
|
1. Make sure the webhook events are being received and processed
|
|
2. Check that the `stripeCustomerId` and `stripeSubscriptionId` fields are correctly populated
|
|
3. Verify that the reference IDs match between your application and Stripe
|
|
|
|
### Testing Webhooks Locally
|
|
|
|
For local development, you can use the Stripe CLI to forward webhooks to your local environment:
|
|
|
|
```bash
|
|
stripe listen --forward-to localhost:3000/api/auth/stripe/webhook
|
|
```
|
|
|
|
This will provide you with a webhook signing secret that you can use in your local environment.
|