# Moving from Stripe to Polar without breaking existing customers

> You cannot transfer live Stripe subscriptions to Polar. Sell new customers on Polar, keep Stripe's webhook alive for the ones you have, move them at renewal, and let the provider-neutral billing rows carry access through the whole overlap.

The question is always the same: "we are on Stripe, we want Polar to handle
the tax, how do we move the subscriptions across?"

You do not. A subscription is a payment mandate between a customer and a
merchant, and Polar is a different merchant. Every real migration is a
re-subscribe, and the work is making that painless. Plan for an overlap of at
least one full billing cycle where both providers are live.

## What already works in your favour

Billing here keeps provider data out of your features:

- Plans live in `src/lib/pricing.ts`, not in either provider.
- Access is read from `billing_subscriptions` and `billing_purchases`, and
  `getEntitlement` does not care which provider wrote a row. Every row has a
  `provider` column (`stripe` or `polar`).
- Feature gates call `hasPlan(userId, "pro")`. Nothing to rewrite.

So a Stripe customer keeps access after you switch, as long as something keeps
their Stripe rows current. That is the whole trick.

## Step 1: create the Polar products

Switch the payments battery to Polar and regenerate (or move the adapter files
by hand), then:

```bash
bun run billing:sync-plans
```

It creates one Polar product per catalogue price and prints the
`BILLING_PRICE_*` lines. Those env vars now hold Polar product ids. Keep your
catalogue ids (`pro-monthly`, `pro-lifetime`) exactly as they were: they are
stored on every existing row.

What does not map one to one:

- **One price per product.** Stripe's product with monthly and yearly prices
  becomes two Polar products.
- **Coupons** become Polar discounts, created again.
- **Trials** come from `trialDays` in the catalogue on both providers, so
  nothing to move.

## Step 2: keep Stripe's webhook alive

New checkouts now go to Polar. Existing Stripe subscribers still renew, cancel
and get refunds on Stripe, and those changes must still reach their rows, or a
cancelled Stripe customer stays entitled forever.

`processWebhook(provider, request)` takes the adapter as an argument, and the
idempotency claim is keyed per provider. So during the overlap keep a second
route:

- Keep Stripe's adapter as `src/lib/billing/stripe-provider.ts` (the old
  `provider.ts`, exporting `stripeProvider`), plus `stripe.ts`,
  `stripe-objects.ts`, the `stripe` dependency and `STRIPE_*` env vars.
- Keep `src/app/api/webhooks/stripe/route.ts` calling
  `processWebhook(stripeProvider, request)`.

The Stripe adapter still names plans from Stripe's lookup keys (the catalogue
ids `billing:sync-plans` set on each Stripe price), so its rows keep their plan
even though `BILLING_PRICE_*` now points at Polar.

## Step 3: move subscribers at renewal

The shared checkout refuses a new subscription while an entitled one exists,
from either provider. That guard stops double billing, and it shapes the move:

**Let them run out (recommended).** Set every Stripe subscription to cancel at
period end, and email each customer before that date with a link to
`/pricing`. When the period ends, Stripe's webhook marks the row cancelled, the
guard lifts, and the customer subscribes on Polar. No double charge, no
refunds. It takes one full cycle, twelve months on yearly plans, and some
customers will not come back.

**Move them now.** Cancel the Stripe subscription immediately with a prorated
refund, then send the Polar checkout link. Faster, but the customer is briefly
without a plan, and the refund must be prompt.

The **Manage billing** button now opens Polar's portal, which does not know
Stripe subscriptions. Cancel those for the customer in Stripe (dashboard or
API) instead of sending them to a portal.

Either way, track it: `select provider, count(*) from billing_subscriptions
where status in ('trialing','active','past_due') group by provider` tells you
how far through you are.

## Lifetime deals carry over as they are

A Stripe lifetime purchase is a `billing_purchases` row with status `paid`. It
keeps granting the plan after the switch, with nothing to migrate. Keep Stripe's
webhook until its refund window has passed, so a refund or dispute still
revokes it.

## Step 4: delete Stripe in one commit

When the last Stripe subscription has ended and the refund window for the last
Stripe purchase has closed, delete the Stripe adapter, its route, its env vars
and the dependency together. Keep the rows: they are your purchase history.
Keep read access to the Stripe dashboard for as long as your records must be
kept.

## Tell customers two things

- **The name on the charge changes.** Their statement and invoice say Polar,
  because Polar is now the seller. Say so in the email, or support fills with
  fraud reports.
- **Invoices come from Polar now.** Business customers reclaiming VAT need the
  new ones; the old Stripe invoices stay in Stripe.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
