Back to Blog
6 min read

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
LT

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.