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 wrapper
  • lib/polar.ts - Polar API wrapper
  • lib/subscriptions.ts - Unified subscription helpers
  • lib/billing/plans.ts - Plan definitions and pricing IDs
  • app/api/stripe/checkout/route.ts - Create Stripe checkout session
  • app/api/stripe/webhook/route.ts - Handle Stripe webhooks
  • app/api/polar/webhook/route.ts - Handle Polar webhooks
  • app/app/billing/page.tsx - User billing page
  • app/(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)

  1. Client calls POST /api/stripe/checkout with { plan }
  2. Server creates Checkout Session with user metadata
  3. Client redirects to Stripe Checkout URL
  4. 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:

  1. Add plan to PLANS in lib/billing/plans.ts
  2. Add STRIPE_PRICE_<PLAN> to .env.example
  3. Update getStripePriceId() function
  4. Add plan card to /pricing page
  5. Update plan type in db/schema.ts