Subscription
Setup and handle subscriptions with Stripe
Setting up a product for subscriptions in Stripe
To create a subscription for a user, you need to create a product and a price object in Stripe. Make sure the price object is set to Recurring
.
Also make sure to create separate products for each plan you want to offer. For example, if you want to offer a Basic
and Pro
plan, you should create two products in Stripe, one for each plan.
You can create a product and price in the Stripe dashboard, or using the Stripe API.
Please also follow the docs on Stripe for more information on how to set up a subscription.
Creating a checkout session
Once you have a product and price, you can create a checkout session. A checkout session represents your customer’s payment session.
This is where you can set the price object, tax, customer, and more.
By default, the boilerplate uses the new embedded UI mode. This means that the checkout will be displayed in an iframe on your site, instead of redirecting the customer to Stripe.
Please see the Stripe checkout docs for more information about the embedded UI mode.
export const load = async ({ locals }) => {
// Get customer details
...
const checkoutSesion = await stripe.checkout.sessions.create({
// Use new embedded UI mode
ui_mode: 'embedded',
// Add your price object(s)
line_items: [
{
price: 'price_YOURPRICEID', // Replace with the ID of your price object
quantity: 1
}
],
mode: 'payment',
return_url: `${ORIGIN}/checkout/return?session_id={CHECKOUT_SESSION_ID}`,
// Adjust these settings below to your needs
automatic_tax: { enabled: true },
allow_promotion_codes: true,
invoice_creation: { enabled: true },
tax_id_collection: { enabled: true },
automatic_tax: { enabled: true },
// Add customer id if exists, otherwise prefill email (and auto create customer)
...(customerId ? { customer: customerId } : {}),
...(customerEmail && !customerId ? { customer_email: customerEmail } : {})
});
// Return the client secret to the client
return { clientSecret: checkoutSesion.client_secret };
};
Add your price obejct id as a variable in your .env
file, and use it in your code. This way you can easily change the price object id and test with different prices and environments.
View the pages and load functions in (app)/account/checkout/
and (app)/account/return/
for an example of how to the checkout on the client and handle the return.
Giving access to the customer
After the customer completes the payment, Stripe sends a checkout.session.completed
and a invoice.created
event to your webhook endpoint. You can use this event to fulfill the customer’s order.
Make sure your webhooks are setup correctly in Stripe, and that you have added your webhook secret to your .env
file. By default, the boilerplate will listen for webhooks on the api/webhooks/stripe
endpoint.
How you handle giving access to a customer is up to you and very much depends on your product and business model.
A simple method can be to add a column plan
to the users
or profiles
table, and set it to pro
or basic
based on the subscription details when the webhook is received.
Then you can check if the user has access to your product, or certain pages within the app, by checking the plan
column.
E.g. if the plan
column is set to pro
, the user has access to the pro features. And if its empty the user has no access.
You can, for example, check for these values in hooks.server.ts
and redirect users to the correct page based on their plan in the Authorization function.
Make sure you add this column to the user or profile table in your database.
// ...
case 'checkout.session.completed': {
const checkoutSession = event.data.object;
// Example with Drizzle ORM, update the user's hasAccess column based on the customer email or stripe customer id
await db
.update(users)
.set({ plan: 'pro' })
.where(
or(
eq(users.email, checkoutSession.customer_email ?? ''),
eq(users.stripeCustomerId, checkoutSession.customer?.toString() ?? '')
)
);
break;
}
// ...
Other webhooks
Read Stripe webhooks to see all the events you can listen for and how this can be used to handle subscriptions.
You can also listen for other events, such as invoice.created
and invoice.paid
to handle the customer’s subscription.
For example, you can listen for the invoice.paid
event to update the user’s plan
column to pro
when the invoice is paid.
// ...
case 'invoice.paid': {
const invoice = event.data.object;
// Example with Drizzle ORM, update the user's hasAccess column based on the customer email or stripe customer id
await db
.update(users)
.set({ plan: 'pro' })
.where(
or(
eq(users.email, invoice.customer_email ?? ''),
eq(users.stripeCustomerId, invoice.customer?.toString() ?? '')
)
);
break;
}
// ...
Handling cancellations
You can also listen for the customer.subscription.deleted
event to handle when a customer cancels their subscription.
// ...
case 'customer.subscription.deleted': {
const subscription = event.data.object;
// Example with Drizzle ORM, update the user's hasAccess column based on the customer email or stripe customer id
await db
.update(users)
.set({ plan: '' })
.where(
or(
eq(users.email, subscription.customer_email ?? ''),
eq(users.stripeCustomerId, subscription.customer?.toString() ?? '')
)
);
break;
}
// ...