In this tutorial we will be going over a full subscription flow with LemonSqueezy.
By the end of the tutorial we will have setup:
Implementing and understanding Lemon-squeezy Subscriptions
Signing up and Creating Products in Lemon Dash
Adding a Subscription with Lemon Hosted Checkout
Managing Subscriptions with Lemon Customer Portal
Working with Lemon webhooks
Lemon Subscription Flow
Similar to the stripe flow, the subscription process requires three main parts
Hosted Checkout to complete a purchase
Webhook to asynchronously update a database after purchase confirmed or subscription update
Hosted Customer Portal to allow customers to update their subscription or payment information
This is the exact same process used by Stripe to set up subscriptions.
We can implement each of these processes below.
Create Products and Variants
To get started we first need to create products and variants in the lemon dashboard. Also signup for a LemonSqueezy account if you haven't already.
Product and Variants are similar to stripe's products and price_ids
For example we can have 2 products with 2 variants. One product could be a Basic tier product with a monthly price variant and yearly price variant. Another can be a Premium tier product also with a monthly and yearly price variant.
This will give us 4 total variant ids. We can add these ids to a .env file
1# Variant Ids
2NEXT_PUBLIC_VARIANT_ID_BASIC_MONTHLY=
3NEXT_PUBLIC_VARIANT_ID_BASIC_YEARLY=
4NEXT_PUBLIC_VARIANT_ID_PREMIUM_MONTHLY=
5NEXT_PUBLIC_VARIANT_ID_PREMIUM_YEARLY=
Store Id and API key
Un like Stripe, lemon squeezy has Stores. We need a store id work with the Lemon Squeezy SDK.
We also need an API key to make authorized request to Lemon
We can add these both to the .env as well.
⚠ Do not prefix the LEMON_API_SECRET_KEY= with NEXT_PUBLIC_, as this will make
the secret available in publicly accessible client code.
1NEXT_PUBLIC_LEMON_STORE_ID=
2LEMON_API_SECRET_KEY=
Initialize lemon-squeezy sdk
After setting our .env variables we can now initialize our sdk.
First we can install the library.
npm install @lemonsqueezy/lemonsqueezy.js
ℹ There is also a Typescript lemon client lemonsqueezy.ts, this is not an official
library and not recommended for use.
Checkout
Below is code for implementing the checkout session. Note we can pass in a user_id from our database to the config. Then this user_id will be available in our webhook event after the user completes checkout. Then in the Webhook we will user this user_id to save the data in our own db.
This is essentially how we will keep track of which user made the purchase after they are redirected away from our front end and into the lemon hosted page.
38 const redirectUrl = await createCheckoutSession({ variant_id, user });
39 redirect(redirectUrl);
40}
41
Billing
Below is the code for retrieving a signed URL for the Lemon customer portal. Simply redirect the user using this URL and they will land of the lemon hosted page in a logged in state.
When a user performs an action in the lemon hosted page that affects their subscription. A subscription.updated event will be sent by the lemon webhook.
We can then listen and use this webhook data to update the customer subscription information in our own database such as plan type or subscription status.
Certain actions done by the user such as updating their payment information does not need to be handled on our end.
We only need the Lemon webhook to send subscription_created and the subscription_updated events, so ensure these are selected in the Lemon webhook settings.
Route Handler
Now we can setup our route:
/api/payments/route.ts
1import { NextResponse } from 'next/server';
2import { headers } from 'next/headers';
3import { WebhookEventHandler } from '@/lib/API/Services/payments/webhook';
There are a few ways we can test our Lemon Subscription. Probably the best way is to use ngrok to set a public url in our local development server and set that url for the Lemon Squeezy webhook endpoint.
Since we are using a hosted page for both checkout and managing subscriptions we don't need to run tests on these hosted pages, as running tests on third party services is considered an anti-pattern.
Therefore our tests will only focus on the webhooks since this is where all our business logic is implemented.
Automation Testing with Playwright
Below we can find an example of an integration test using playwright, that tests our webhook.
Essentially we can use prisma or another database ORM to make mock db queries, then running assertions based on the results of the queries.
22export type EventName = WebhookPayload['meta']['event_name'];
23
24export type Subscription = {
25 type: 'subscriptions';
26 id: string;
27 attributes: {
28 store_id: number;
29 order_id: number;
30 customer_id: number;
31 order_item_id: number;
32 product_id: number;
33 variant_id: number;
34 product_name: string;
35 variant_name: string;
36 user_name: string;
37 user_email: string;
38 status: SubscriptionStatus;
39 status_formatted: string;
40 pause: any | null;
41 cancelled: boolean;
42 trial_ends_at: string | null;
43 billing_anchor: number;
44 urls: {
45 update_payment_method: string;
46 };
47 renews_at: string;
48 /**
49 * If the subscription has as status of cancelled or expired, this will be an ISO-8601 formatted date-time string indicating when the subscription expires (or expired). For all other status values, this will be null.
50 */
51 ends_at: string | null;
52 created_at: string;
53 updated_at: string;
54 test_mode: boolean;
55 };
56};
57
58export type SubscriptionStatus =
59 | 'on_trial'
60 | 'active'
61 | 'paused'
62 | 'past_due'
63 | 'unpaid'
64 | 'cancelled'
65 | 'expired';
66
Conclusion
This is it. We now have setup a professional level subscription system using Lemon Squeezy