Billing
Subscription management with Stripe or Polar. Both providers write to a unified subscriptions table, so you can switch providers without changing application code.
Architecture
- Stripe logic lives only in
lib/stripe.ts - Polar logic lives only in
lib/polar.ts - All subscription writes go through
lib/subscriptions.ts - Plan definitions are in
lib/billing/plans.ts
Never import Stripe/Polar SDKs directly in components.
Key files
lib/stripe.ts- Stripe API wrapperlib/polar.ts- Polar API wrapperlib/subscriptions.ts- Unified subscription helperslib/billing/plans.ts- Plan definitions and pricing IDsapp/api/stripe/checkout/route.ts- Create Stripe checkout sessionapp/api/stripe/webhook/route.ts- Handle Stripe webhooksapp/api/polar/webhook/route.ts- Handle Polar webhooksapp/app/billing/page.tsx- User billing pageapp/(marketing)/pricing/page.tsx- Public pricing page
Environment variables
Stripe:
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
STRIPE_PRICE_FOUNDER=price_xxx
STRIPE_PRICE_PRO=price_xxx
Polar:
POLAR_ACCESS_TOKEN=polar_xxx
POLAR_WEBHOOK_SECRET=xxx
POLAR_PLAN_FOUNDER=xxx
POLAR_PLAN_PRO=xxx
Config:
NEXT_PUBLIC_BILLING_PROVIDER=stripe
Subscription data model
{
userId: string;
provider: "stripe" | "polar";
providerCustomerId: string;
providerSubscriptionId: string;
plan: "free" | "founder" | "pro";
status: "active" | "canceled" | "past_due" | "trialing";
currentPeriodEnd: Date | null;
}
Reading subscription state
import { isSubscribed, getCurrentPlan } from "@/lib/subscriptions";
const hasAccess = await isSubscribed(userId);
const plan = await getCurrentPlan(userId); // "free" | "founder" | "pro"
Checkout flow (Stripe)
- Client calls
POST /api/stripe/checkoutwith{ plan } - Server creates Checkout Session with user metadata
- Client redirects to Stripe Checkout URL
- Webhook handles subscription creation
Webhook setup
Stripe: https://yourdomain.com/api/stripe/webhook
Events: checkout.session.completed, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted
Polar: https://yourdomain.com/api/polar/webhook
Events: subscription.created, subscription.updated, subscription.canceled
Adding a new plan
Use the /add-plan Cursor command, or manually:
- Add plan to
PLANSinlib/billing/plans.ts - Add
STRIPE_PRICE_<PLAN>to.env.example - Update
getStripePriceId()function - Add plan card to
/pricingpage - Update plan type in
db/schema.ts