Open-source project
lmsqueezy/nextjs-billing avatar
lmsqueezy/nextjs-billing

nextjs-billing: a Lemon Squeezy subscription starter for Next.js 14

Next.js billing app with Lemon Squeezy

740 stars106 forksTypeScriptMIT

At a glance

What is it?
nextjs-billing is a Next.js 14 demo app that wires Lemon Squeezy subscriptions to Auth.js, Drizzle and Neon so a SaaS project can skip the first month of billing plumbing. It processes only two webhook events by design, and the README tells you plainly that Lemon Squeezy's own customer portal already covers more than the app does.
Who is it for?
Adopt nextjs-billing when you are building a subscription SaaS on Next.js 14 and want Lemon Squeezy checkout, GitHub login and a Postgres schema wired together this week rather than next month, and you are willing to own the two-event webhook model.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 128 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A starting point for subscription SaaS, not a billing product

The framing in the README is a starting point, and it is worth taking literally. This Next.js demo app is meant to be used as a base for building subscription-based SaaS apps: you clone the repository and build your app alongside the ready-made auth and billing. The deliverable is a working starting point, not a finished product.

What the template actually provides is the wiring that every subscription product ends up needing. A checkout that talks to Lemon Squeezy, a login that talks to GitHub, a database that records the relationship between a user and a subscription, and a webhook endpoint that receives subscription changes. None of that is intellectually hard, and all of it is annoying to write from scratch with the right error handling.

The audience is a developer with a Next.js 14 app in mind who has decided to bill through Lemon Squeezy. The alternative audience, someone who wants a full-featured billing system in the box, is explicitly discouraged in the README. It notes that Lemon Squeezy comes with a built-in customer portal that covers all the features from this app and more, and only argues that the template is worth using if you want a billing solution more closely integrated with your own platform.

That framing is the most important thing in the repository, because it sets the boundary. What you are getting is integration, and the cost of integration is that you now own the parts the portal would have handled.

The stack: Next.js 14, Auth.js, Drizzle, Neon and Wedges

The README lists the stack explicitly, and the package.json versions match it. The framework is Next.js 14, pinned in the dependencies as next 14.2.35, with the App Router, which the README says includes support for enhanced layouts, colocation of components, tests and styles, and component-level data fetching. The language is TypeScript. Billing is Lemon Squeezy through the @lemonsqueezy/lemonsqueezy.js SDK. Auth is GitHub OAuth through Auth.js v5, with next-auth pinned to a 5.0.0 beta. The ORM is Drizzle with drizzle-orm, the styling is Tailwind CSS, the components come from Wedges, and the database is serverless Postgres on Neon via @neondatabase/serverless.

Two details in that list deserve attention. The next-auth dependency is a beta, and Auth.js v5 itself is described in the README as version 5, so a team that prefers a stable major of the auth library should know what they are getting. The Neon choice is not arbitrary: the README explains that Neon plus Drizzle ORM for serverless Postgres is what makes the template compatible with Vercel Edge functions and serverless deployments.

That edge compatibility is a stated goal, and it constrains the design. The `pg` package is in the dependencies, and the drizzle.config.ts file at the root handles the ORM configuration. The app also ships a linting and formatting toolchain: ESLint with the Next.js and Tailwind plugins, Prettier with its own packagejson and tailwindcss plugins, plus `typecheck` and `format` scripts. For a template that people will fork, having the lint and format rules already in place is a small courtesy that saves an afternoon.

From clone to running app in four commands

The setup path is short. The README asks you to clone the repo and navigate into the directory, then install the project dependencies with pnpm, which tells you the package manager the project expects. The lock file in the repository is pnpm-lock.yaml, so a pnpm install resolves the same versions the template was built against.

bash
pnpm install

Next, copy the environment file. The repository ships a .env.example, and the README tells you to copy it to .env and fill in the values:

bash
cp .env.example .env

The variables the example file expects are these:

bash
LEMONSQUEEZY_API_KEY=
LEMONSQUEEZY_STORE_ID=
LEMONSQUEEZY_WEBHOOK_SECRET=
WEBHOOK_URL=
POSTGRES_URL=
AUTH_GITHUB_ID=
AUTH_GITHUB_SECRET=
AUTH_SECRET=
AUTH_URL=
NEXT_PUBLIC_APP_URL=

The first three come from the Lemon Squeezy store. The README tells you to make sure you are in Test mode, then create a new API key under Settings > API, and to take the store ID from the list at Settings > Stores. The webhook secret is one you invent yourself, and the README explains its purpose: it is a security key that ensures data received from a webhook is genuine and unaltered.

For the auth secret, the README suggests generating one on Linux or macOS with a specific command:

bash
openssl rand -hex 32

The database schema is pushed with Drizzle, and Drizzle Studio gives you a visual editor for the same data:

bash
pnpm db:push
pnpm db:studio

Finally, start the development server and open http://localhost:3000 in a browser:

bash
pnpm dev

The npm scripts behind these are plain, which is a good sign for a template:

json
    "build": "next build",
    "db:push": "drizzle-kit push",
    "db:studio": "drizzle-kit studio",
    "dev": "next dev",

Webhooks: two events, one route, and a typo in the instructions

The README calls webhook setup a required step, and it is the part most likely to cost you an afternoon. Lemon Squeezy has to know where to send events, so you configure webhooks in your store under Settings > Webhooks, and the app includes a Setup webhook button that demonstrates creating a webhook through the Lemon Squeezy SDK from a server action.

The scope is deliberately small. The README says to check at least two events, `subscription_created` and `subscription_updated`, and states plainly that the app demo only processes these two events, while noting they are enough to get a billing system in place. It then suggests the obvious extension: handle successful payment events such as `subscription_payment_success` if you want to list invoices in your billing system.

The endpoint is `/api/webhook`, and the signing secret you add in the Lemon Squeezy form must match the value in `LEMONSQUEEZY_WEBHOOK_SECRET`.

Here is the trap. The README says that if you are manually setting up the webhook, you need to append `/api/webook` to your webhook URL, and the very next sentence gives the example as `https://your-app-url.com/api/webhook`. The word is misspelled in the instruction but correct in the example, and the SDK-based setup path appends the correct path automatically. If you follow the manual path literally you will register a URL that 404s, and the events will never arrive.

That single typo is a good argument for using the Setup webhook button rather than the manual form, at least the first time.

Local webhooks need a tunnel, and that constrains local development

Your local app has to be reachable from the internet for Lemon Squeezy to deliver events, and the README is direct about the consequence: this is not available when running your site on its local server without some sort of tunnel. The suggestion is a service like ngrok or an app like LocalCan, and once you have a URL you put it in `WEBHOOK_URL`.

The .env.example file adds a comment on the same point, noting that webhooks require a public URL and that ngrok can expose a local server to the internet. It also records that the webhook endpoint, `/api/webhook`, will be automatically appended to the URL, which tells you the variable is the base URL and the path is added for you.

This is a real constraint on the development loop rather than a documentation gap. Every developer on the team needs a tunnel, and every developer needs to remember to update the Lemon Squeezy store when their tunnel URL changes, since a new ngrok URL means the previously registered webhook is pointing at nothing. The alternative is to point the Lemon Squeezy store at a deployed staging environment and work against that, which avoids the churn but means the webhook target is a real deployment rather than a local process.

The rest of the local configuration is more forgiving. `AUTH_URL` takes the app URL in the form `https://your-app-url.com/api/auth`, and `http://localhost:3000/api/auth` works for local development, while `NEXT_PUBLIC_APP_URL` is the plain app URL, for example `http://localhost:3000`. The .env.example also records the GitHub OAuth callback URL format, `http://your-site.com/api/auth/callback/github`, which is the value to paste into the OAuth app you create on GitHub.

Going live means swapping test mode for a live API key

The production section lists the steps for taking the app live. You turn off Test mode in your Lemon Squeezy store and add a new live mode API key, then add that key as an environment variable in your live server using the same name `LEMONSQUEEZY_API_KEY`. The store ID stays the same, since it is the same store, only the key changes.

The rest of the repository is set up for this. The README says the template is compatible with Vercel Edge Functions and serverless deployments, and the Neon plus Drizzle combination is what makes the database layer work in that environment. A .env.example file with no secrets in it, a .gitignore, and an environment-variable driven configuration all point the same way: the deployment target expects the same variables to be set in the hosting platform.

Two gaps are worth naming before you rely on this. The first is that the README does not walk through migrating from the local development database to the production one, so the `pnpm db:push` step has to be reasoned about rather than copied. The second is that the webhook secret and the OAuth secret are described as values you set yourself, with no rotation procedure given for either.

On the licence, the repository is MIT. That is a factual reading of the identifier and not legal advice, but it does mean you can take the code, change it and ship it in a proprietary product, which is exactly the intent behind a template like this. The one thing to keep separate is the licence of the template from the terms of the services it depends on, Lemon Squeezy, Neon and GitHub, which come with their own terms and pricing.

Editorial conclusion

Adopt nextjs-billing when you are building a subscription SaaS on Next.js 14 and want Lemon Squeezy checkout, GitHub login and a Postgres schema wired together this week rather than next month, and you are willing to own the two-event webhook model. Do not adopt it if your product needs invoices, usage billing or multiple payment methods, because the app processes only subscription_created and subscription_updated and the README itself points at the hosted customer portal for a wider feature set. Before building on it, confirm the app and API versions in package.json still match your Lemon Squeezy store, note that the dependency on next-auth is pinned to a 5.0.0 beta, and read the webhook section, since the README contains a typo in the manual setup path.

Frequently asked questions

How do I set up webhooks in nextjs-billing?

The README calls it a required step: configure webhooks in your Lemon Squeezy store under Settings > Webhooks, and make sure the signing secret matches the value of `LEMONSQUEEZY_WEBHOOK_SECRET`. The app provides a Setup webhook button that creates the webhook through the Lemon Squeezy SDK, and the endpoint is `/api/webhook`.

Which Lemon Squeezy events does nextjs-billing handle?

The app demo processes two events, `subscription_created` and `subscription_updated`, which the README says are enough to get a billing system in place. It suggests subscribing to events such as `subscription_payment_success` if you extend the app to list invoices.

Do I need Lemon Squeezy instead of the built-in customer portal?

The README notes that Lemon Squeezy comes with a built-in customer portal that covers all the features from this app and more, and positions the template as a foundation for a billing solution more closely integrated with your own SaaS platform. The choice depends on whether you want that integration or the hosted portal.

Official sources

  1. Issues
  2. License: MIT
  3. lmsqueezy/nextjs-billing on GitHub
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/lmsqueezy-nextjs-billing.svg)](https://hysenlabs.com/projects/lmsqueezy-nextjs-billing)

Was this useful?