mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 11:24:57 -05:00
1051 lines
37 KiB
Plaintext
1051 lines
37 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.
|
|
|
|
## 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
|
|
- **Automatic trial abuse prevention** - Users can only get one trial per account across all plans
|
|
- 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@^20.0.0
|
|
```
|
|
</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!, {
|
|
apiVersion: "2025-11-17.clover", // Latest API version as of Stripe SDK v20.0.0
|
|
})
|
|
|
|
export const auth = betterAuth({
|
|
// ... your existing config
|
|
plugins: [
|
|
stripe({
|
|
stripeClient,
|
|
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
|
|
createCustomerOnSignUp: true,
|
|
})
|
|
]
|
|
})
|
|
```
|
|
|
|
<Callout type="info">
|
|
**Upgrading from Stripe v18?** Version 19 uses async webhook signature verification (`constructEventAsync`) which is handled internally by the plugin. No code changes required on your end!
|
|
</Callout>
|
|
</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 authClient = 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">
|
|
```package-install
|
|
npx @better-auth/cli migrate
|
|
```
|
|
</Tab>
|
|
<Tab value="generate">
|
|
```package-install
|
|
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.created`
|
|
- `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.
|
|
|
|
When you set `createCustomerOnSignUp: true`, a Stripe customer is automatically created on signup and 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 ({ stripeCustomer, user }, ctx) => {
|
|
// Do something with the newly created customer
|
|
console.log(`Customer ${stripeCustomer.id} created for user ${user.id}`);
|
|
},
|
|
getCustomerCreateParams: async (user, ctx) => {
|
|
// 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:
|
|
|
|
<APIMethod
|
|
path="/subscription/upgrade"
|
|
method="POST"
|
|
requireSession
|
|
>
|
|
```ts
|
|
type upgradeSubscription = {
|
|
/**
|
|
* The name of the plan to upgrade to.
|
|
*/
|
|
plan: string = "pro"
|
|
/**
|
|
* Whether to upgrade to an annual plan.
|
|
*/
|
|
annual?: boolean = true
|
|
/**
|
|
* Reference id of the subscription. Defaults based on customerType.
|
|
*/
|
|
referenceId?: string = "123"
|
|
/**
|
|
* The id of the subscription to upgrade.
|
|
*/
|
|
subscriptionId?: string = "sub_123"
|
|
/**
|
|
* Additional metadata to store with the subscription.
|
|
*/
|
|
metadata?: Record<string, any>
|
|
/**
|
|
* The type of customer for billing. (Default: "user")
|
|
*/
|
|
customerType?: "user" | "organization"
|
|
/**
|
|
* Number of seats to upgrade to (if applicable).
|
|
*/
|
|
seats?: number = 1
|
|
/**
|
|
* The IETF language tag of the locale Checkout is displayed in.
|
|
* If not provided or set to `auto`, the browser's locale is used.
|
|
*/
|
|
locale?: string
|
|
/**
|
|
* The URL to which Stripe should send customers when payment or setup is complete.
|
|
*/
|
|
successUrl: string
|
|
/**
|
|
* If set, checkout shows a back button and customers will be directed here if they cancel payment.
|
|
*/
|
|
cancelUrl: string
|
|
/**
|
|
* The URL to return to from the Billing Portal (used when upgrading existing subscriptions)
|
|
*/
|
|
returnUrl?: string
|
|
/**
|
|
* Disable redirect after successful subscription.
|
|
*/
|
|
disableRedirect: boolean = false
|
|
}
|
|
```
|
|
</APIMethod>
|
|
|
|
**Simple Example:**
|
|
|
|
```ts title="client.ts"
|
|
await authClient.subscription.upgrade({
|
|
plan: "pro",
|
|
successUrl: "/dashboard",
|
|
cancelUrl: "/pricing",
|
|
annual: true, // Optional: upgrade to an annual plan
|
|
referenceId: "org_123", // Optional: defaults based on customerType
|
|
seats: 5, // Optional: for team plans
|
|
locale: "en" // Optional: display checkout in English
|
|
});
|
|
```
|
|
|
|
This will create a Checkout Session and redirect the user to the Stripe Checkout page.
|
|
|
|
<Callout type="info">
|
|
The plugin only supports one active or trialing subscription per reference ID (user or organization) at a time. Multiple concurrent subscriptions for the same reference ID are not supported.
|
|
|
|
If the user already has an active subscription, you **must** provide the `subscriptionId` parameter when upgrading. Otherwise, a new subscription may be created alongside the existing one, resulting in duplicate billing.
|
|
</Callout>
|
|
|
|
> **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 authClient.subscription.upgrade({
|
|
plan: "pro",
|
|
successUrl: "/dashboard",
|
|
cancelUrl: "/pricing",
|
|
});
|
|
if(error) {
|
|
alert(error.message);
|
|
}
|
|
```
|
|
|
|
#### Switching Plans
|
|
|
|
To switch a subscription to a different plan, use the `subscription.upgrade` method:
|
|
```ts title="client.ts"
|
|
await authClient.subscription.upgrade({
|
|
plan: "pro",
|
|
successUrl: "/dashboard",
|
|
cancelUrl: "/pricing",
|
|
subscriptionId: "sub_123", // the Stripe subscription ID of the user's current plan
|
|
});
|
|
```
|
|
This ensures that the user only pays for the new plan, and not both.
|
|
|
|
#### Listing Active Subscriptions
|
|
|
|
To get the user's active subscriptions:
|
|
|
|
<APIMethod
|
|
path="/subscription/list"
|
|
method="GET"
|
|
requireSession
|
|
resultVariable="subscriptions"
|
|
>
|
|
```ts
|
|
type listActiveSubscriptions = {
|
|
/**
|
|
* Reference id of the subscription to list.
|
|
*/
|
|
referenceId?: string = '123'
|
|
/**
|
|
* The type of customer for billing. (Default: "user")
|
|
*/
|
|
customerType?: "user" | "organization"
|
|
}
|
|
|
|
// get the active subscription
|
|
const activeSubscription = subscriptions.find(
|
|
sub => sub.status === "active" || sub.status === "trialing"
|
|
);
|
|
|
|
// Check subscription limits
|
|
const projectLimit = subscriptions?.limits?.projects || 0;
|
|
```
|
|
</APIMethod>
|
|
|
|
Make sure to provide `authorizeReference` in your plugin config to authorize the reference ID
|
|
|
|
```ts title="auth.ts"
|
|
stripe({
|
|
// ... other options
|
|
subscription: {
|
|
// ... other subscription options
|
|
authorizeReference: async ({ user, session, referenceId, action }) => {
|
|
if(action === "list-subscription") {
|
|
const org = await db.member.findFirst({
|
|
where: {
|
|
organizationId: referenceId,
|
|
userId: user.id
|
|
}
|
|
});
|
|
return org?.role === "owner"
|
|
}
|
|
// Check if the user has permission to list subscriptions for this reference
|
|
return true;
|
|
}
|
|
}
|
|
})
|
|
```
|
|
#### Canceling a Subscription
|
|
|
|
To cancel a subscription:
|
|
|
|
<APIMethod
|
|
path="/subscription/cancel"
|
|
method="POST"
|
|
requireSession
|
|
>
|
|
```ts
|
|
type cancelSubscription = {
|
|
/**
|
|
* Reference id of the subscription to cancel. Defaults based on customerType.
|
|
*/
|
|
referenceId?: string = 'org_123'
|
|
/**
|
|
* The type of customer for billing. (Default: "user")
|
|
*/
|
|
customerType?: "user" | "organization"
|
|
/**
|
|
* The id of the subscription to cancel.
|
|
*/
|
|
subscriptionId?: string = 'sub_123'
|
|
/**
|
|
* URL to take customers to when they click on the billing portal's link to return to your website.
|
|
*/
|
|
returnUrl: string = '/account'
|
|
}
|
|
```
|
|
</APIMethod>
|
|
|
|
This will redirect the user to the Stripe Billing Portal where they can cancel their subscription.
|
|
|
|
<Callout type="info">
|
|
**Understanding Cancellation States**
|
|
|
|
Stripe supports different types of cancellation, and the plugin tracks all of them:
|
|
|
|
| Field | Description |
|
|
|---------------------|----------------------------------------------------------------------------------------------------------------------------------|
|
|
| `cancelAtPeriodEnd` | Whether this subscription will (if status=active) or did (if status=canceled) cancel at the end of the current billing period. |
|
|
| `cancelAt` | If the subscription is scheduled to be canceled, this is the time at which the cancellation will take effect. |
|
|
| `canceledAt` | If the subscription has been canceled, this is the time when it was canceled. |
|
|
| `endedAt` | If the subscription has ended, the date the subscription ended. |
|
|
| `status` | Changes to "canceled" only after the subscription has actually ended. |
|
|
</Callout>
|
|
|
|
#### Restoring a Canceled Subscription
|
|
|
|
> <small className='font-normal'>**Note:** This only works for subscriptions that are still active but scheduled to cancel. It cannot restore subscriptions that have already ended (`status: "canceled"` with `endedAt` set).</small>
|
|
|
|
If a user changes their mind after canceling a subscription (but before the subscription period ends), you can restore the subscription:
|
|
|
|
|
|
<APIMethod
|
|
path="/subscription/restore"
|
|
method="POST"
|
|
requireSession
|
|
>
|
|
```ts
|
|
type restoreSubscription = {
|
|
/**
|
|
* Reference id of the subscription to restore. Defaults based on customerType.
|
|
*/
|
|
referenceId?: string = '123'
|
|
/**
|
|
* The type of customer for billing. (Default: "user")
|
|
*/
|
|
customerType?: "user" | "organization"
|
|
/**
|
|
* The id of the subscription to restore.
|
|
*/
|
|
subscriptionId?: string = 'sub_123'
|
|
}
|
|
```
|
|
</APIMethod>
|
|
|
|
|
|
This will reactivate a subscription that was previously scheduled to cancel. The subscription will continue to renew automatically.
|
|
|
|
<Callout type="info">
|
|
When a subscription is restored:
|
|
- `cancelAtPeriodEnd` is set to `false`
|
|
- `cancelAt` is cleared to `null`
|
|
- `canceledAt` is cleared to `null`
|
|
</Callout>
|
|
|
|
|
|
#### Creating Billing Portal Sessions
|
|
|
|
To create a [Stripe billing portal session](https://docs.stripe.com/api/customer_portal/sessions/create) where customers can manage their subscriptions, update payment methods, and view billing history:
|
|
|
|
<APIMethod
|
|
path="/subscription/billing-portal"
|
|
method="POST"
|
|
requireSession
|
|
>
|
|
```ts
|
|
type createBillingPortal = {
|
|
/**
|
|
* The IETF language tag of the locale Customer Portal is displayed in.
|
|
* If not provided or set to `auto`, the browser's locale is used.
|
|
*/
|
|
locale?: string
|
|
/**
|
|
* Reference id of the subscription.
|
|
*/
|
|
referenceId?: string = "123"
|
|
/**
|
|
* The type of customer for billing. (Default: "user")
|
|
*/
|
|
customerType?: "user" | "organization"
|
|
/**
|
|
* Return URL to redirect back after exiting the billing portal.
|
|
*/
|
|
returnUrl?: string
|
|
/**
|
|
* Disable the automatic redirect to the billing page.
|
|
* @default false
|
|
*/
|
|
disableRedirect?: boolean = false
|
|
}
|
|
```
|
|
</APIMethod>
|
|
<Callout type="info" >
|
|
For supported locales, see the [IETF language tag documentation](https://docs.stripe.com/js/appendix/supported_locales).
|
|
</Callout>
|
|
|
|
This endpoint creates a Stripe billing portal session and returns a URL in the response as `data.url`. You can redirect users to this URL to allow them to manage their subscription, payment methods, and billing history.
|
|
|
|
### 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 authClient.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 authClient.subscription.list({
|
|
query: {
|
|
referenceId: "org_123456"
|
|
}
|
|
});
|
|
```
|
|
|
|
#### Team Subscriptions with Seats
|
|
|
|
For team or organization plans, you can specify the number of seats:
|
|
|
|
```ts
|
|
await authClient.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" || action === "restore-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.created`: Creates a subscription when created outside the checkout flow
|
|
- `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 via checkout
|
|
await sendWelcomeEmail(subscription.referenceId, plan.name);
|
|
},
|
|
onSubscriptionCreated: async ({ event, subscription, stripeSubscription, plan }) => {
|
|
// Called when a subscription is created outside the checkout flow (e.g. Stripe dashboard)
|
|
await sendSubscriptionCreatedEmail(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 }, ctx) => {
|
|
// Called when a trial ends
|
|
await sendTrialEndEmail(subscription.referenceId);
|
|
},
|
|
onTrialExpired: async (subscription, ctx) => {
|
|
// 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",
|
|
isOptional: true
|
|
},
|
|
]}
|
|
/>
|
|
|
|
### Organization
|
|
|
|
Table Name: `organization` <small className='text-xs'>(only when `organization.enabled` is `true`)</small>
|
|
|
|
<DatabaseTable
|
|
fields={[
|
|
{
|
|
name: "stripeCustomerId",
|
|
type: "string",
|
|
description: "The Stripe customer ID for the organization",
|
|
isOptional: true
|
|
},
|
|
]}
|
|
/>
|
|
|
|
### 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). This should NOT be a unique field in your database, as it must allow users to resubscribe after a cancellation.",
|
|
isUnique: false
|
|
},
|
|
{
|
|
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: "cancelAt",
|
|
type: "Date",
|
|
description: "If the subscription is scheduled to be canceled, this is the time at which the cancellation will take effect",
|
|
isOptional: true
|
|
},
|
|
{
|
|
name: "canceledAt",
|
|
type: "Date",
|
|
description: "If the subscription has been canceled, this is the time when the cancellation was requested. Note: If the subscription was canceled with cancelAtPeriodEnd, this reflects the cancellation request time, not when the subscription actually ends",
|
|
isOptional: true
|
|
},
|
|
{
|
|
name: "endedAt",
|
|
type: "Date",
|
|
description: "If the subscription has ended, this is the date the subscription ended",
|
|
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
|
|
|
|
| Option | Type | Description |
|
|
| ------------------------- | ---------- | --------------------------------------------------------------------------------------------- |
|
|
| `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` | `function` | Callback called after a customer is created. Receives `{ stripeCustomer, user }` and context. |
|
|
| `getCustomerCreateParams` | `function` | Customize Stripe customer creation parameters. Receives `user` and context. |
|
|
| `onEvent` | `function` | Callback called for any Stripe webhook event. Receives `Stripe.Event`. |
|
|
| `subscription` | `object` | Subscription configuration. See [below](#subscription-options). |
|
|
| `organization` | `object` | Enable Organization Customer support. See [below](#organization-options). |
|
|
| `schema` | `object` | Customize the database schema for the Stripe plugin. |
|
|
|
|
### Subscription Options
|
|
|
|
| Option | Type | Description |
|
|
| -------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
| `enabled` | `boolean` | Whether to enable subscription functionality. **Required.** |
|
|
| `plans` | `StripePlan[]` or `function` | An array of subscription plans or an async function that returns plans. **Required** if enabled. |
|
|
| `requireEmailVerification` | `boolean` | Whether to require email verification before allowing subscription upgrades. Default: `false`. |
|
|
| `authorizeReference` | `function` | Authorize reference IDs. Receives `{ user, session, referenceId, action }` and context. |
|
|
| `getCheckoutSessionParams` | `function` | Customize Stripe Checkout session parameters. Receives `{ user, session, plan, subscription }`, request, and context. |
|
|
| `onSubscriptionComplete` | `function` | Called when a subscription is created via checkout. Receives `{ event, stripeSubscription, subscription, plan }` and context. |
|
|
| `onSubscriptionCreated` | `function` | Called when a subscription is created outside checkout. Receives `{ event, stripeSubscription, subscription, plan }`. |
|
|
| `onSubscriptionUpdate` | `function` | Called when a subscription is updated. Receives `{ event, subscription }`. |
|
|
| `onSubscriptionCancel` | `function` | Called when a subscription is canceled. Receives `{ event, subscription, stripeSubscription, cancellationDetails }`. |
|
|
| `onSubscriptionDeleted` | `function` | Called when a subscription is deleted. Receives `{ event, stripeSubscription, subscription }`. |
|
|
|
|
#### Plan Configuration
|
|
|
|
| Option | Type | Description |
|
|
| ------------------------- | ---------- | ------------------------------------------------------------ |
|
|
| `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. |
|
|
| `annualDiscountLookupKey` | `string` | The Stripe price lookup key for annual billing. |
|
|
| `limits` | `object` | Limits for plan (e.g. `{ projects: 10, storage: 5 }`). |
|
|
| `group` | `string` | A group name for categorizing plans. |
|
|
| `freeTrial` | `object` | Trial configuration. See [below](#free-trial-configuration). |
|
|
|
|
#### Free Trial Configuration
|
|
|
|
| Option | Type | Description |
|
|
| ---------------- | ---------- | ---------------------------------------------------------------------------------------- |
|
|
| `days` | `number` | Number of trial days. **Required.** |
|
|
| `onTrialStart` | `function` | Called when a trial starts. Receives `subscription`. |
|
|
| `onTrialEnd` | `function` | Called when a trial ends. Receives `{ subscription }` and context. |
|
|
| `onTrialExpired` | `function` | Called when a trial expires without conversion. Receives `subscription` and context. |
|
|
|
|
### Organization Options
|
|
|
|
| Option | Type | Description |
|
|
| ------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
| `enabled` | `boolean` | Enable Organization Customer support. **Required.** |
|
|
| `getCustomerCreateParams` | `function` | Customize Stripe customer creation parameters for organizations. Receives `organization` and context. |
|
|
| `onCustomerCreate` | `function` | Called after an organization customer is created. Receives `{ stripeCustomer, organization }` and context. |
|
|
|
|
## Advanced Usage
|
|
|
|
### Using with Organizations
|
|
|
|
The Stripe plugin integrates with the [organization plugin](/docs/plugins/organization) to enable organizations as Stripe Customers. Instead of individual users, organizations become the billing entity for subscriptions. This is useful for B2B services where billing is tied to the organization rather than individual user.
|
|
|
|
<Callout type="info">
|
|
**When Organization Customer is enabled:**
|
|
|
|
- A Stripe Customer is automatically created when an organization first subscribes
|
|
- Organization name changes are synced to the Stripe Customer
|
|
- Organizations with active subscriptions cannot be deleted
|
|
</Callout>
|
|
|
|
#### Enabling Organization Customer
|
|
|
|
To enable Organization Customer, set `organization.enabled` to `true` and ensure the organization plugin is installed:
|
|
|
|
```ts title="auth.ts"
|
|
plugins: [
|
|
organization(),
|
|
stripe({
|
|
// ... other options
|
|
subscription: {
|
|
enabled: true,
|
|
plans: [...],
|
|
},
|
|
organization: { // [!code highlight]
|
|
enabled: true // [!code highlight]
|
|
} // [!code highlight]
|
|
})
|
|
]
|
|
```
|
|
|
|
#### Creating Organization Subscriptions
|
|
|
|
Even with Organization Customer enabled, user subscriptions remain available and are the default. To use the organization as the billing entity, pass `customerType: "organization"`:
|
|
|
|
```ts title="client.ts"
|
|
await authClient.subscription.upgrade({
|
|
plan: "team",
|
|
referenceId: activeOrg.id,
|
|
customerType: "organization", // [!code highlight]
|
|
seats: 10,
|
|
successUrl: "/org/billing/success",
|
|
cancelUrl: "/org/billing"
|
|
});
|
|
```
|
|
|
|
#### Authorization
|
|
|
|
Make sure to implement the `authorizeReference` function to verify that the user has permission to manage subscriptions for the organization:
|
|
|
|
```ts title="auth.ts"
|
|
subscription: {
|
|
// ... other subscription options
|
|
authorizeReference: async ({ user, referenceId, action }) => {
|
|
const member = await db.members.findFirst({
|
|
where: {
|
|
userId: user.id,
|
|
organizationId: referenceId
|
|
}
|
|
});
|
|
|
|
return member?.role === "owner" || member?.role === "admin";
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Organization Billing Email
|
|
|
|
Unlike users, organization billing email is not automatically synced because organization itself doesn't have a unique email. Organizations often use a dedicated billing email separate from user accounts.
|
|
To change the billing email after checkout, update it through the Stripe Dashboard or implement custom logic using `stripeClient`:
|
|
|
|
```ts
|
|
await stripeClient.customers.update(organization.stripeCustomerId, {
|
|
email: "billing@company.com"
|
|
});
|
|
```
|
|
|
|
### Custom Checkout Session Parameters
|
|
|
|
You can customize the Stripe Checkout session with additional parameters:
|
|
|
|
```ts title="auth.ts"
|
|
getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
|
|
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 collect tax IDs from the customer, set `tax_id_collection` to true:
|
|
|
|
```ts title="auth.ts"
|
|
subscription: {
|
|
// ... other options
|
|
getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
|
|
return {
|
|
params: {
|
|
tax_id_collection: {
|
|
enabled: true
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
### Automatic Tax Calculation
|
|
|
|
To enable automatic tax calculation using the customer's location, set `automatic_tax` to true. Enabling this parameter causes Checkout to collect any billing address information necessary for tax calculation. You need to have tax registration setup and configured in the Stripe dashboard first for this to work.
|
|
|
|
```ts title="auth.ts"
|
|
subscription: {
|
|
// ... other options
|
|
getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
|
|
return {
|
|
params: {
|
|
automatic_tax: {
|
|
enabled: true
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
### Trial Period Management
|
|
|
|
The Stripe plugin automatically prevents users from getting multiple free trials. Once a user has used a trial period (regardless of which plan), they will not be eligible for additional trials on any plan.
|
|
|
|
**How it works:**
|
|
- The system tracks trial usage across all plans for each user
|
|
- When a user subscribes to a plan with a trial, the system checks their subscription history
|
|
- If the user has ever had a trial (indicated by `trialStart`/`trialEnd` fields or `trialing` status), no new trial will be offered
|
|
- This prevents abuse where users cancel subscriptions and resubscribe to get multiple free trials
|
|
|
|
**Example scenario:**
|
|
1. User subscribes to "Starter" plan with 7-day trial
|
|
2. User cancels the subscription after the trial
|
|
3. User tries to subscribe to "Premium" plan - no trial will be offered
|
|
4. User will be charged immediately for the Premium plan
|
|
|
|
This behavior is automatic and requires no additional configuration. The trial eligibility is determined at the time of subscription creation and cannot be overridden through configuration.
|
|
|
|
## 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.
|