logo png

YourEpicSaaS

Features

Billing & Subscriptions

How Paddle subscriptions, pricing plans, webhooks, and the billing portal work in the boilerplate.


Overview

Billing is handled entirely by Paddle. Each organization has its own subscription — users subscribe their org to a plan, not their personal account.

The boilerplate ships with:

  • A pricing page with plan cards (from app.config.ts)
  • A Paddle Checkout embedded in the app
  • A webhook handler that keeps subscription state in sync
  • A subscription management portal (cancel, change plan)

Setting Up Paddle

Create Products & Prices in Paddle

  1. Log in to your Paddle dashboard
  2. Go to Catalog → Products → create a product per plan (e.g. Starter, Pro)
  3. For each product, add a monthly price and a yearly price
  4. Copy each Price ID (format: pri_01...)

Update app.config.ts

Paste the Price IDs into src/app.config.ts:

paddle: [
  {
    name: "Starter",
    description: "For solo founders.",
    features: ["1 workspace", "Basic support"],
    recommended: false,
    priceId: {
      month: "pri_01abc...",  // monthly Price ID from Paddle
      year:  "pri_01def...",  // yearly Price ID from Paddle
    },
  },
  {
    name: "Pro",
    description: "For growing teams.",
    features: ["Unlimited workspaces", "Priority support"],
    recommended: true,
    priceId: {
      month: "pri_01ghi...",
      year:  "pri_01jkl...",
    },
  },
],

The pricing cards UI reads directly from this config — no code changes needed.

Add Environment Variables

NEXT_PUBLIC_PADDLE_ENV=sandbox          # Use "production" when going live
NEXT_PUBLIC_PADDLE_CLIENT_TOKEN=...     # Paddle → Developer Tools → Client-side token
PADDLE_API_KEY=...                      # Paddle → Developer Tools → API keys
PADDLE_NOTIFICATION_WEBHOOK_SECRET=...  # Generated when you create a webhook endpoint

Use sandbox mode during development. Switch to production only when you're ready to accept real payments.

Configure the Webhook

  1. In Paddle → Developer Tools → Notifications
  2. Create a new endpoint pointing to:
    https://your-app.vercel.app/api/webhook/paddle
  3. Enable these events:
    • subscription.created
    • subscription.updated
    • subscription.canceled
    • transaction.completed
  4. Copy the Webhook Secret → set as PADDLE_NOTIFICATION_WEBHOOK_SECRET

For local webhook testing, use the Paddle CLI or a tunnel like ngrok to forward events to localhost:3000.


How the Billing Flow Works

User clicks "Subscribe" on a plan

Paddle Checkout opens (embedded)

Payment completes in Paddle

Paddle sends webhook to /api/webhook/paddle

Boilerplate saves subscription to `subscriptions` table

User redirected to /org/[orgId]/billing/checkout/success

The subscriptions table stores the Paddle subscription ID, status, and links it to the organization. All subscription reads go through Paddle's API to get live data — the local table is a foreign-key reference.


Billing Portal

Each organization's billing page is at /org/[orgId]/billing. It shows:

SectionWhat it displays
Current planPlan name, status, next billing date
Subscription statusActive / Past Due / Canceled / Trialing
Change planSwitch to a different price (upgrade/downgrade)
Cancel subscriptionCancels at end of current billing period

Canceling

Cancellations are handled via cancel-subscription.tsx. Paddle cancels at the end of the period — the subscription status stays active until then and changes to canceled on the next webhook event.

Changing Plans

Plan changes go through change-subscription.tsx. Paddle handles proration automatically.


Subscription Statuses

StatusMeaning
activeSubscription is live and paid
past_duePayment failed — Paddle will retry
canceledSubscription ended
trialingOn a free trial
pausedSubscription is paused

The subscription-alerts.tsx component renders contextual banners based on status — for example, a warning banner when the subscription is past due.


Database Table

ColumnPurpose
subscription_idPaddle subscription ID
organization_idWhich org this subscription belongs to
statusCurrent subscription status
price_idActive Paddle price ID

Troubleshooting

Pricing cards don't show plans

  • Check that paddle array in app.config.ts has at least one entry with valid priceId values

Checkout doesn't open

  • Verify NEXT_PUBLIC_PADDLE_CLIENT_TOKEN and NEXT_PUBLIC_PADDLE_ENV are set
  • In sandbox mode, use Paddle test card numbers

Webhook events not received

  • Confirm the endpoint URL in Paddle matches exactly (including /api/webhook/paddle)
  • Check PADDLE_NOTIFICATION_WEBHOOK_SECRET matches what Paddle generated
  • View incoming events in Paddle → Developer Tools → Notifications → Logs

Subscription not showing after payment

  • The webhook may be delayed — refresh after a few seconds
  • Check your server logs for webhook processing errors