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
- Log in to your Paddle dashboard
- Go to Catalog → Products → create a product per plan (e.g. Starter, Pro)
- For each product, add a monthly price and a yearly price
- 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 endpointUse sandbox mode during development. Switch to production only when
you're ready to accept real payments.
Configure the Webhook
- In Paddle → Developer Tools → Notifications
- Create a new endpoint pointing to:
https://your-app.vercel.app/api/webhook/paddle - Enable these events:
subscription.createdsubscription.updatedsubscription.canceledtransaction.completed
- 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/successThe 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:
| Section | What it displays |
|---|---|
| Current plan | Plan name, status, next billing date |
| Subscription status | Active / Past Due / Canceled / Trialing |
| Change plan | Switch to a different price (upgrade/downgrade) |
| Cancel subscription | Cancels 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
| Status | Meaning |
|---|---|
active | Subscription is live and paid |
past_due | Payment failed — Paddle will retry |
canceled | Subscription ended |
trialing | On a free trial |
paused | Subscription 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
| Column | Purpose |
|---|---|
subscription_id | Paddle subscription ID |
organization_id | Which org this subscription belongs to |
status | Current subscription status |
price_id | Active Paddle price ID |
Troubleshooting
Pricing cards don't show plans
- Check that
paddlearray inapp.config.tshas at least one entry with validpriceIdvalues
Checkout doesn't open
- Verify
NEXT_PUBLIC_PADDLE_CLIENT_TOKENandNEXT_PUBLIC_PADDLE_ENVare 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_SECRETmatches 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
