LemonSqueezy is used to handle payments. It has built-in VAT/sales tax compliance, affiliates & marketing tools.

Set up LemonSqueezy

First, create an LemonSqueezy account or sign in. Create a store & add your digital products. It usually takes 1-2 days for it to get verified.

Once that is ready, we’ll set up the app to handle webhooks from LemonSqueezy:

  1. Go to Settings -> Webhooks and add a new Webhook

    Add a new webhook to LemonSqueezy

  2. Fill in Callback URL with https://yourwebsite.com/api/payments/lemonsqueezy & select all events.

  3. Add your defined LEMON_SQUEEZY_WEBHOOK_SECRET to your .env file.

    Select all events & specify your LEMON_SQUEEZY_WEBHOOK_SECRET

Handling webhooks

Under app/api/payments/lemonsqueezy/route.ts you’ll find an API route that handles both one-time-payments and subscriptions from LemonSqueezy.

It will do the following:

  1. Verify if the request is legitimate by matching LEMON_SQUEEZY_WEBHOOK_SECRET
  2. Get purchased product, based on the variant_id
  3. Match the user email with our profiles table
    • If there is a matching user, it will edit purchase column of the matching user in the profiles table based on the logic you specify in the webhook. Feel free to add any logic here you’ll use in your app to handle page access.
    • If no matching user is found, it will simply update the purchases table with a new entry

Storing webhook data

We’ll store all webhook data coming from LemonSqueezy in our own database as well.

The purchases table should already exist if you followed the Quick Setup guide and ran the Supabase migrations using the CLI:

npx supabase db push

If you haven’t done this yet, please go back to the Quick Setup guide first. Alternatively, you can manually create the table by running the SQL from this file in your Supabase SQL editor: supabase/migrations/20240000000007_purchases.sql

Make sure RLS is turned on. We don’t need to add any rules as we’ll be using the service key for to interact with this table.

Set up paywall

All demo apps include a paywall using a credit system. You can modify the logic in app/api/payments/lemonsqueezy/route.ts to add more credits after a succesfull purchase.

You can adjust the code to handle access based on the purchase column having a specific value. This allows for handling lifetime access systems or subscriptions.

Logic flow

The codebase includes a credit-based paywall system that uses cached queries for better performance. Here’s how it works:

import {
  getSession,
  getUserCredits,
  getUserRecordings,
} from "@/lib/db/cached-queries";

export default async function Page() {
  const user = await getSession();

  let credits;
  let recordings;

  if (user) {
    if (toolConfig.paywall) {
      credits = await getUserCredits(user.id);

      if (credits < toolConfig.credits) {
        return <PaymentModal />;
      }
    }
    // ... rest of the code

The paywall logic:

  1. Check if the demo app is paywalled (specified in toolConfig.ts).
  2. Uses cached queries to efficiently verify user credit balance:
    • If yes, grant access.
    • If no, display the PaymentModal component.

The cached queries system helps reduce database load and improve performance compared to direct Supabase queries.