Billing with Stripe and Polar.sh
LaunchApp ships with two billing providers out of the box: Stripe and Polar.sh. Both are wrapped behind the @repo/billing package, which exposes the same interface regardless of which provider is active. You pick one at deploy time by setting the BILLING_PROVIDER env var — the rest of the codebase never cares which is running.
- billing
- stripe
- polar
LaunchApp Team
Mar 4, 2025
LaunchApp ships with two billing providers out of the box: Stripe and Polar.sh. Both are wrapped behind the @repo/billing package, which exposes the same interface regardless of which provider is active. You pick one at deploy time by setting the BILLING_PROVIDER env var — the rest of the codebase never cares which is running.
Why two providers
Stripe is the default for most SaaS businesses: mature tax handling, global card coverage, and a well-known dashboard. Polar.sh is optimized for developer tooling — merchant-of-record billing (they handle VAT/sales tax for you), and a cleaner flow for one-off purchases and digital products.
Rather than force a choice, the template wires both up and lets you pick per project. Swapping providers is an env-var change, not a refactor.
The checkout flow
Every checkout starts with a call to billing.createCheckoutSession() which returns a URL to redirect to. Both providers accept the same shape:
const { url } = await billing.createCheckoutSession({
userId: session.user.id,
priceId: "price_pro_monthly",
successUrl: `${origin}/dashboard/billing?status=success`,
cancelUrl: `${origin}/pricing`,
});
return redirect(url);
The abstraction hides provider-specific parameters like mode, line_items, and customer_email behind a single normalized interface. If you outgrow the abstraction and need provider-specific features, you can import the raw provider client from @repo/billing/stripe or @repo/billing/polar directly.
Webhooks
Each provider posts events to a webhook endpoint: /api/webhooks/stripe and /api/webhooks/polar. Both handlers verify the signature, decode the event, and forward it to a shared event reducer in @repo/billing/events. The reducer updates the subscription table in @repo/database and emits a typed BillingEvent that the rest of the app can subscribe to.
Keep subscription state in your own database — don't treat the provider as the source of truth. This lets you query "who has an active subscription" without a round-trip to Stripe or Polar, and survives short outages on their side.
Testing locally
Use the Stripe CLI (stripe listen --forward-to localhost:3000/api/webhooks/stripe) to forward test events to your local API. Polar has a similar CLI. Both are documented in docs/billing.md and covered by pnpm test in packages/billing.