Claude Code Boilerplate
FeaturesPricingBlogDocs
Get started →

Product

  • Features
  • Pricing
  • Skills

Compare

  • vs ShipFast
  • vs MakerKit
  • vs supastarter

Resources

  • Docs
  • Blog
  • Discord

Legal

  • License
  • Privacy Policy
  • Terms of Service
Claude Code Boilerplate

© 2026 Claude Code Boilerplate. All rights reserved.

← All posts

How to Offer a Lifetime Deal for Your Next.js SaaS -- One-Time Payment, Permanent Access

August 19, 2026
nextjsstripedrizzle-ormsaaslaunch

You have a SaaS. You want your first 200 customers fast -- ideally before you have to run ads or wait for SEO to kick in. A lifetime deal is one of the fastest ways to get there. AppSumo, Product Hunt, and your own mailing list will convert people who will never subscribe monthly but will happily pay once for permanent access.

The problem is that most SaaS stacks are built around subscriptions. Adding a lifetime option means your access-check logic needs to handle two paths: "is this user subscribed?" and "did this user buy lifetime access?" Get that wrong and you either lock out paying customers or hand out free access to anyone who asks.

Here is the exact pattern to add a lifetime deal to a Next.js SaaS that already has Stripe subscriptions -- about an hour of work.

The access model

Subscription access and lifetime access are different things in your database. The cleanest approach is a single boolean on the user record: lifetimeAccess. Your gating logic then becomes:

has access = active subscription OR lifetimeAccess === true

No third table, no state machine, no scheduled cleanup job. When someone buys the LTD, you flip the flag. It never expires.

Schema change

Add one column to your user table in modules/user/user.schema.ts:

lifetimeAccess: boolean("lifetime_access").notNull().default(false),

Run npm run db:generate and npm run db:migrate. Existing users default to false -- nothing breaks.

Stripe product setup

Create a new product in Stripe with a one-time price, not a recurring price. Copy the price ID and add it to your environment:

NEXT_PUBLIC_STRIPE_PRICE_LTD=price_REPLACE_ME

Keep it separate from your subscription price IDs. Stripe treats one-time and recurring products differently at the checkout level.

Checkout route

Your existing subscription route creates a Stripe Session in subscription mode. For the LTD, add a new route in payment mode:

const session = await stripe.checkout.sessions.create({
  mode: "payment",
  line_items: [{ price: process.env.NEXT_PUBLIC_STRIPE_PRICE_LTD, quantity: 1 }],
  customer_email: user.email,
  metadata: { userId: user.id, type: "ltd" },
  success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/dashboard?ltd=success`,
  cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
});

The metadata.type field is how your webhook knows this is a lifetime purchase, not a subscription.

Webhook handler

In your Stripe webhook handler, add a branch for checkout.session.completed where type === "ltd":

if (event.type === "checkout.session.completed") {
  const session = event.data.object;
  if (session.metadata?.type === "ltd") {
    await userRepo.setLifetimeAccess(session.metadata.userId, true);
  }
}

setLifetimeAccess is one update query in your user repository. The webhook fires within seconds of payment. The user reloads the page and has full access.

For more on handling Stripe webhooks reliably in this stack, see Stripe subscriptions in Next.js.

Access check

Wherever you currently check for an active subscription, extend the condition:

const hasAccess = user.lifetimeAccess || isSubscriptionActive(user.subscriptionStatus);

If you have a shared getUserAccess helper or middleware, update it in one place. Do not copy-paste this check across every protected route.

What to put on your pricing page

Add an LTD tier next to your monthly and annual options. Be specific about what it includes. Common patterns that work:

  • Cap it at a specific plan tier (e.g. "everything in Pro, forever")
  • Limit the number of seats or workspaces included
  • Make it time-limited ("available through [date]")

A genuine deadline converts better than a permanent "buy once" button. Scarcity you can actually close is not a trick -- it is an honest offer with a window.

When a lifetime deal makes sense

LTDs work best in two situations. You are launching and need your first wave of customers who will talk about your product publicly. Or you are listing on a platform like AppSumo that has a built-in audience of deal hunters who rarely subscribe monthly but often become vocal advocates.

Do not keep the LTD open forever. It creates pricing confusion and undercuts your recurring revenue. Run it for two to eight weeks, close it, and move on. The urgency is the point.

Ready to ship it

This Next.js boilerplate already has Stripe subscriptions, JWT auth, Drizzle ORM, and webhook infrastructure wired up. Adding a lifetime deal is a schema column, a new checkout route, and one webhook branch -- not a rebuild. You can ship it before your launch email goes out.