DEV Community

Dmitriy Malakhov
Dmitriy Malakhov

Posted on

Build a Complete SaaS with Next.js, Supabase & Creem - Subscriptions, Credits, Licenses in 30 Minutes

So you want to launch a SaaS product. You need auth, a database, payments, subscription management, license keys, a credits system, discount codes, webhooks... and it all needs to work together.

I built SaaSKit - an open-source boilerplate that wires all of this up with Next.js 16, Supabase, and Creem. In this tutorial, I'll walk you through how it works and how to get your own SaaS running.

Live demo: https://nextjs-supabase-creem-boilerplate.vercel.app/
GitHub: https://github.com/malakhov-dmitrii/nextjs-supabase-creem-boilerplate

Boilerplate landing screenshot


Why Creem?

If you've used Stripe, you know the pain: tax compliance, VAT handling, international regulations. Creem is a Merchant of Record - they handle all of that for you. 3.9% + 30c, no monthly fees, and a TypeScript SDK that's a joy to use.

They also have built-in license key management, which is rare for payment processors.


What You Get

SaaSKit ships with:

  • Authentication -- Email/password + Google/GitHub OAuth via Supabase
  • 3-tier pricing -- Starter ($9), Pro ($29), Enterprise ($99)
  • Subscription management -- Upgrade, downgrade, cancel (scheduled or immediate)
  • Seat-based billing -- Add/remove team members
  • 13 webhook events -- Full subscription lifecycle, refunds, disputes
  • License keys -- Activate, validate, deactivate per device
  • Credits wallet -- Atomic Postgres operations, no race conditions
  • Discount codes -- Percentage or fixed amount, product-scoped
  • Email notifications -- Welcome + payment confirmation via Resend
  • Rate limiting -- Upstash Redis on sensitive routes
  • SEO -- Sitemap, robots.txt, OG image generation
  • Demo mode -- Works without any accounts, pre-seeded with data
  • 131 tests -- All importing from actual source code
  • CI/CD -- GitHub Actions pipeline

Quick Start (Demo Mode)

No accounts needed. Clone and run:

git clone https://github.com/malakhov-dmitrii/nextjs-supabase-creem-boilerplate saaskit
cd saaskit
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:3000. Demo mode activates automatically - you'll see a pre-seeded Pro subscription, 50 credits, and a sample license key. The full checkout flow works with in-memory data.


Architecture Overview

The project uses Next.js 16 App Router with a clean structure:

src/
  app/
    (auth)/          -- Login, signup, OAuth callback
    api/             -- 11 API routes (checkout, subscriptions, licenses, credits, etc.)
    dashboard/       -- Main dashboard, transactions, licenses, admin
    pricing/         -- Pricing page with discount code support
  components/        -- 14 client components
  lib/
    creem.ts         -- SDK client with auto test/prod detection
    email.ts         -- Resend integration with graceful fallback
    rate-limit.ts    -- Upstash rate limiting
    demo/            -- Demo mode: store, mock, detection
    supabase/        -- Browser, server, admin clients
Enter fullscreen mode Exit fullscreen mode

Setting Up Payments with Creem

1. Install the SDK

SaaSKit uses both the Creem SDK and the Next.js webhook adapter:

npm install creem @creem_io/nextjs
Enter fullscreen mode Exit fullscreen mode

2. Create a Checkout

The checkout route creates a Creem hosted checkout session:

// src/app/api/checkout/route.ts
const checkout = await creem.checkouts.create({
  productId,
  successUrl: `${appUrl}/dashboard?checkout=success`,
  discountCode: discountCode || undefined,
  metadata: { user_id: user.id },
});
return NextResponse.json({ url: checkout.checkoutUrl });
Enter fullscreen mode Exit fullscreen mode

The metadata.user_id is critical - it's how webhooks know which user to update.

3. Handle Webhooks

SaaSKit handles all 13 Creem webhook events using @creem_io/nextjs:

// src/app/api/webhooks/creem/route.ts
Webhook({
  webhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
  onCheckoutCompleted: async (event) => {
    // Upsert subscription, send confirmation email
  },
  onSubscriptionCanceled: async (event) => {
    // Update status to cancelled
  },
  onGrantAccess: async ({ reason, metadata }) => {
    // Set subscription active
  },
  onRevokeAccess: async ({ reason, metadata }) => {
    // Revoke access based on reason
  },
  // ... 9 more handlers
});
Enter fullscreen mode Exit fullscreen mode

Every webhook handler includes idempotency checking via a webhook_events table - duplicate events are detected and skipped.


Credits System

The credits wallet uses atomic Postgres operations to prevent race conditions:

CREATE FUNCTION spend_credits(p_user_id uuid, p_amount int, p_reason text)
RETURNS integer AS $$
  -- Lock row, check balance (handles unlimited sentinel -1)
  -- Deduct and log transaction atomically
$$ LANGUAGE plpgsql SECURITY DEFINER;
Enter fullscreen mode Exit fullscreen mode

Enterprise plans get unlimited credits (stored as -1 sentinel). The app layer checks isUnlimited() before calling the RPC, and the SQL function handles it as a defense-in-depth measure.


License Key Management

Creem has built-in license key support. SaaSKit exposes three API routes:

// Activate a license on a device
await creem.licenses.activate({ key, instanceName });

// Validate it's still active
await creem.licenses.validate({ key, instanceId });

// Deactivate when done
await creem.licenses.deactivate({ key, instanceId });
Enter fullscreen mode Exit fullscreen mode

License keys are delivered automatically via the checkout.completed webhook and displayed in the dashboard.


Demo Mode

One of SaaSKit's unique features is a full demo mode that works without any external services:

export function isDemoMode(): boolean {
  const url = process.env.NEXT_PUBLIC_SUPABASE_URL ?? "";
  return PLACEHOLDER_URLS.includes(url);
}
Enter fullscreen mode Exit fullscreen mode

When no real Supabase URL is configured, the app uses an in-memory store with:

  • A pre-seeded Pro subscription
  • 50 credits with transaction history
  • A sample active license key
  • Full mock Supabase client supporting selects, inserts, upserts, and RPC calls

This means judges, reviewers, or potential users can clone the repo and see everything working immediately.


Testing Strategy

SaaSKit has 131 unit tests across 19 test files, plus Playwright E2E specs. Every test imports from the actual source module - no reimplemented logic.

The key pattern: extract pure functions into handlers.ts, validators.ts, and helpers.ts files, then test those directly:

// tests/api/webhook-handler.test.ts
import { mapSubscriptionStatus, buildSubscriptionUpsert } from "@/app/api/webhooks/creem/handlers";

test("maps subscription.canceled to cancelled", () => {
  expect(mapSubscriptionStatus("subscription.canceled")).toBe("cancelled");
});
Enter fullscreen mode Exit fullscreen mode

Deploying to Vercel

# Push to GitHub, then:
# 1. Import repo in Vercel
# 2. Add environment variables
# 3. Deploy
Enter fullscreen mode Exit fullscreen mode

After deployment, configure your Creem webhook URL to https://your-app.vercel.app/api/webhooks/creem and you're live.


Wrapping Up

SaaSKit gives you a production-ready foundation:

  • 12 video proofs demonstrating every flow
  • 131 honest tests with CI/CD
  • Demo mode for instant evaluation
  • 13 webhook handlers for full subscription lifecycle
  • Rate limiting, email, SEO out of the box

The code is MIT licensed. Clone it, customize it, ship your SaaS.

GitHub: https://github.com/malakhov-dmitrii/nextjs-supabase-creem-boilerplate
Live Demo: https://nextjs-supabase-creem-boilerplate.vercel.app/


Built for the Creem Scoops bounty program. Creem is a Merchant of Record for SaaS - learn more at creem.io.

Top comments (0)