lmsqueezy/nextjs-billing

repository·main·Indexed 20 days ago

https://github.com/lmsqueezy/nextjs-billing

A Next.js 14 starter template for building subscription-based SaaS applications. It integrates Lemon Squeezy for billing, Auth.js for authentication, and Drizzle ORM with Neon Postgres for database management. The template includes features for syncing plans, managing user subscription lifecycles (cancel, pause, change plans), and handling webhooks via the Lemon Squeezy SDK.

Tokens
5.8K
Snippets
22
Records
27
Agent score
72%

What's inside lmsqueezy-nextjs-billing

  1. Quickstart: Install and run the Next.js Billing App

    main

    Follow these steps to get the project running locally:

    1. Clone the repository and enter the directory.
    2. Install dependencies using pnpm:
      pnpm install
    3. Configure environment variables: Copy the example file:
      cp .env.example .env
      Fill in the required keys (see Environment Variables Reference).
    4. Set up the database: Push the schema to your Postgres instance:
      pnpm db:push
    5. Start the development server:
      pnpm dev
      The app will be available at http://localhost:3000.
    pnpm install
    cp .env.example .env
    pnpm db:push
    pnpm dev
  2. Deploying to Production

    main

    When moving from development to production, perform the following steps:

    1. Switch Lemon Squeezy Mode: Turn off Test mode in your Lemon Squeezy store.
    2. Update API Key: Generate a new API key for Live mode and update LEMONSQUEEZY_API_KEY in your production environment.
    3. Update Webhooks: Create a new webhook in your live store pointing to your production URL (e.g., https://your-production-app.com/api/webhook).
    4. Update Webhook Secret: Ensure the new webhook's signing secret is reflected in your production LEMONSQUEEZY_WEBHOOK_SECRET environment variable.

    Note: The LEMONSQUEEZY_STORE_ID remains the same for both test and live modes.

  3. Set up Lemon Squeezy Webhooks

    main

    Webhooks are required for the app to process subscription changes.

    Manual Setup

    1. Go to Settings > Webhooks in your Lemon Squeezy store.
    2. Set the webhook URL to your app's endpoint: https://your-app-url.com/api/webhook.
    3. Select at least these two events:
      • subscription_created
      • subscription_updated
    4. Add a signing secret in Lemon Squeezy and ensure the exact same value is set in your LEMONSQUEEZY_WEBHOOK_SECRET environment variable.

    Automated Setup

    The app includes a Setup webhook button (a server action) that uses the Lemon Squeezy SDK to create the webhook for you automatically, including the correct endpoint path.

  4. Manage the database with Drizzle

    main

    This project uses Drizzle ORM with Neon Postgres.

    To push your schema changes to the database:

    pnpm db:push

    To inspect your data using Drizzle Studio, run:

    pnpm db:studio

    This will open a local instance of Drizzle Studio at https://local.drizzle.studio/.

    pnpm db:push
    pnpm db:studio
  5. Configure Lemon Squeezy integration

    main

    Use the configureLemonSqueezy function to initialize the Lemon Squeezy JS SDK and validate that all necessary environment variables are present. This function must be called during your application's setup phase to ensure the SDK is correctly configured before any billing operations occur.

    Required Environment Variables

    The following variables must be defined in your .env file:

    • LEMONSQUEEZY_API_KEY: Your Lemon Squeezy API key.
    • LEMONSQUEEZY_STORE_ID: Your Lemon Squeezy store identifier.
    • LEMONSQUEEZY_WEBHOOK_SECRET: The secret used to validate incoming webhooks.

    If any of these variables are missing, the function will throw an error listing the specific missing keys.

    import { configureLemonSqueezy } from './src/config/lemonsqueezy';
    
    // Call this during app initialization
    configureLemonSqueezy();
  6. Configure Drizzle ORM with drizzle.config.ts

    main

    The project uses drizzle-kit for database migrations and schema management. The configuration is defined in drizzle.config.ts and requires a POSTGRES_URL environment variable to connect to the database.

    Key configuration options used in this project:

    • schema: Points to the source of truth for the database schema (located at ./src/db/schema.ts).
    • out: Specifies the directory where migration files will be generated (./src/db/migrations).
    • dialect: Set to postgresql.
    • dbCredentials: Contains the connection url derived from the POSTGRES_URL environment variable.
    • verbose: Enabled to provide detailed output during CLI operations.
    • strict: Enabled to ensure type safety and strictness during migrations.
    import { defineConfig } from "drizzle-kit";
    
    export default defineConfig({
      schema: "./src/db/schema.ts",
      out: "./src/db/migrations",
      dialect: "postgresql",
      dbCredentials: { url: process.env.POSTGRES_URL! },
      verbose: true,
      strict: true,
    });
  7. Configure Environment Variables

    main

    The application requires several environment variables to function. Populate these in your .env file:

    Lemon Squeezy

    • LEMONSQUEEZY_API_KEY: Your API key from Settings > API. Ensure you are in Test mode for development.
    • LEMONSQUEEZY_STORE_ID: Your store ID found in Settings > Stores.
    • LEMONSQUEEZY_WEBHOOK_SECRET: A random string used to verify webhook signatures.
    • WEBHOOK_URL: A public URL (e.g., via ngrok or LocalCan) that Lemon Squeezy can reach to send events to your app.

    Database

    • POSTGRES_URL: Your connection string from your Neon account.

    Authentication (Auth.js)

    • AUTH_GITHUB_ID: The Client ID from your GitHub OAuth app.
    • AUTH_GITHUB_SECRET: The Client Secret from your GitHub OAuth app.
    • AUTH_SECRET: A random 32-byte hex string. Generate it using:
      openssl rand -hex 32
    • AUTH_URL: The full URL for your auth endpoint (e.g., http://localhost:3000/api/auth for local dev).

    App Configuration

    • NEXT_PUBLIC_APP_URL: The base URL of your application (e.g., http://localhost:3000).
  8. Sync Lemon Squeezy plans with the database

    main

    The syncPlans action synchronizes product variants from your Lemon Squeezy store with your local plans database table.

    Behavior:

    1. It fetches all products and their variants from Lemon Squeezy.
    2. It filters for variants where the category is subscription.
    3. It skips draft variants or pending variants if multiple exist.
    4. It calculates pricing (handling both usage-based and fixed pricing), intervals, and trial periods.
    5. It performs an upsert (onConflictDoUpdate) into the plans table using variantId as the target.

    This should be run whenever you add or modify subscription products in the Lemon Squeezy dashboard.

    // Syncs Lemon Squeezy subscription variants to your local DB
    const updatedPlans = await syncPlans();
  9. Format dates to a short localized string

    main

    Use formatDate to convert a date (string, number, Date object, or null/undefined) into a localized US English string with the format: Month Day, Year (e.g., Jan 01, 2024). If no valid date is provided, it returns an empty string.

    import { formatDate } from "@/lib/utils";
    
    const dateStr = formatDate(new Date()); // "Oct 24, 2023"
    const empty = formatDate(null); // ""
  10. Validate active subscription status

    main

    Use isValidSubscription to determine if a Lemon Squeezy subscription status represents an active, usable subscription. A subscription is considered valid only if its status is not cancelled, expired, or unpaid.

    import { isValidSubscription } from "@/lib/utils";
    
    // status is derived from Subscription["data"]["attributes"]["status"]
    const isActive = isValidSubscription("active"); // true
    const isInvalid = isValidSubscription("cancelled"); // false
  11. Verify required Lemon Squeezy environment variables

    main

    Call checkRequiredEnv to ensure the application has the necessary Lemon Squeezy credentials configured. This function throws an error if either LEMONSQUEEZY_API_KEY or LEMONSQUEEZY_STORE_ID is missing from process.env.

    import { checkRequiredEnv } from "@/lib/utils";
    
    // Call this during app initialization or in a server-side entry point
    try {
      checkRequiredEnv();
    } catch (error) {
      console.error(error.message);
    }
  12. Merge Tailwind classes with `cn`

    main

    The cn utility combines clsx and tailwind-merge. Use it to conditionally apply CSS classes and ensure that Tailwind class conflicts are resolved correctly (the last class passed wins).

    import { cn } from "@/lib/utils";
    
    // Resolves conflicts: 'p-4' is overridden by 'p-2'
    const className = cn("p-4 text-red-500", "p-2"); // "p-2 text-red-500"
    
    // Conditional classes
    const dynamicClass = cn("base-class", isActive && "active-class");