Polar Billing
Configure lifetime and recurring Polar checkout, customer management and application access.
Set PAYMENT_PROVIDER=polar to use Polar for new lifetime and monthly/yearly
subscription checkout. Stripe remains the default. Existing purchases and
subscriptions keep their provider ownership and management paths when the
checkout provider changes. Recurring checkout requires a signed-in account.
To remove an unused integration, follow Use One Billing Provider. Changing the checkout provider alone preserves installed readers, portals and webhook handling for existing billing.
Prepare the local application
Complete local setup, then apply pending feature migrations from the template root:
bunx supabase migration up --localThe Polar migrations add checkout/purchase, subscription and delivery-claim tables. They preserve existing Stripe records. Do not reset a database that contains data you need to keep.
Connect the CLI to the sandbox
Install the official Polar CLI, then check its commands:
polar --version
polar --helpCreate a sandbox organization access token
for the local workflow. Catalog creation and inspection need
organizations:read and products:write; checkout and fulfillment need
checkouts:write and orders:read; the CLI listener needs webhooks:write.
Add discounts:write or refunds:write when testing those operations.
Subscription lifecycle operations need subscriptions:write; customer
portal creation needs customer_sessions:write.
A single development token can serve both the CLI and local application.
Enter it privately in your terminal, without putting its value in shell history or command arguments:
printf 'Polar sandbox token: '
stty -echo
IFS= read -r POLAR_ACCESS_TOKEN
stty echo
printf '\n'
export POLAR_ACCESS_TOKEN
export POLAR_ENVIRONMENT=sandbox
polar organizations get <sandbox-organization-id> --fields id,name,slug --jsonStop if the returned organization is not your intended sandbox organization. Token environment variables override the CLI's saved OAuth session. Always set the environment explicitly: token authentication defaults to production. Sandbox accounts, tokens and product IDs are separate from production.
Create the recurring offers
The supplied payloads define Starter, Pro and Business, each as a separate monthly and yearly fixed-price product. Create the six offers through the native CLI:
for offer in starter-monthly starter-yearly pro-monthly pro-yearly business-monthly business-yearly; do
polar products create --org <sandbox-organization-id> --data "$(cat "supabase/polar/subscriptions/$offer.json")" --fields id,name,prices --json
done
polar products list --org <sandbox-organization-id> --is-recurring=true --fields id,name,recurring_interval,prices --jsonKeep the payload amounts and displayed plans in src/config/pricing.config.ts
aligned. The server accepts the source tier and cadence, then verifies its
configured product, organization, recurring interval and single fixed price.
Missing mappings leave that offer unavailable; they never select Stripe as
a fallback.
Create the lifetime offers
Create one fixed, one-time price per product. These commands match the example Individual and Team prices; replace their names and amounts when adapting your product:
polar products create --org <sandbox-organization-id> --data '{"name":"Individual","recurring_interval":null,"prices":[{"amount_type":"fixed","price_currency":"usd","price_amount":29900}]}' --json
polar products create --org <sandbox-organization-id> --data '{"name":"Team","recurring_interval":null,"prices":[{"amount_type":"fixed","price_currency":"usd","price_amount":79900}]}' --json
polar products list --org <sandbox-organization-id> --is-recurring=false --jsonThe integration supports a single active fixed catalog price on each
configured product. Recurring, custom, free and ad-hoc price types are not
lifetime offers. Keep displayed prices/features in
src/config/pricing.config.ts aligned with your provider catalog.
Configure the application and listener
Save the development token in ignored .env.local; keep it server-only.
The application itself uses checkouts:write, products:read,
orders:read, subscriptions:read and customer_sessions:write.
The CLI's catalog and test operations need the additional
development permissions above. Add the organization and product IDs:
PAYMENT_PROVIDER=polar
POLAR_ENVIRONMENT=sandbox
POLAR_ORGANIZATION_ID=<sandbox-organization-id>
POLAR_INDIVIDUAL_PRODUCT_ID=<individual-product-id>
POLAR_TEAM_PRODUCT_ID=<team-product-id>
POLAR_STARTER_PRODUCT_ID_MONTHLY=<starter-monthly-product-id>
POLAR_STARTER_PRODUCT_ID_YEARLY=<starter-yearly-product-id>
POLAR_PRO_PRODUCT_ID_MONTHLY=<pro-monthly-product-id>
POLAR_PRO_PRODUCT_ID_YEARLY=<pro-yearly-product-id>
POLAR_BUSINESS_PRODUCT_ID_MONTHLY=<business-monthly-product-id>
POLAR_BUSINESS_PRODUCT_ID_YEARLY=<business-yearly-product-id>
POLAR_ACCESS_TOKEN=<sandbox-organization-access-token>In your own terminal, start the listener:
polar webhook listen http://localhost:3000/api/webhooks/polar --org <sandbox-organization-id>The listener displays a signing secret. Copy it into POLAR_WEBHOOK_SECRET
in ignored .env.local, then start or restart bun run dev. Keep the secret
out of command arguments, screenshots, logs and commits. Use your app's
actual local port if it differs from 3000.
Open /pricing. The browser sends a source tier ID; the server selects the
configured provider product. The application creates Checkout through the
SDK. The current CLI exposes checkout inspection, not checkout creation.
Open /pricing/subscription while signed in to choose a recurring offer.
Customer Sessions for the Polar portal also use the SDK because CLI 3.0.0
does not expose that resource.
Prove a real sandbox purchase
You can first run the account-free regression tests:
bun run testThese exercise application decisions and signed fixtures with controlled provider responses. They do not prove a real payment or database round trip.
For guest onboarding proof, configure EMAIL_DELIVERY_MODE=test_capture,
a server-only TEST_EMAIL_CAPTURE_TOKEN, and POLAR_TEST_EMAIL with a
mailbox belonging to your sandbox organization. The test uses a unique
plus-address alias. Restart the app after changing settings.
The lifetime suite requires both POLAR_INDIVIDUAL_PRODUCT_ID and
POLAR_TEAM_PRODUCT_ID: the returning guest buys the second lifetime offer
after setting up the account. The subscription suite requires both
POLAR_PRO_PRODUCT_ID_MONTHLY and POLAR_PRO_PRODUCT_ID_YEARLY. These are
prerequisites for the maintained test journeys; your application only needs
mappings for the offers you sell.
Keep the app and listener running, then run:
./coremvp e2e billing:lifetime
./coremvp e2e billing:subscriptionWith Polar selected, this suite covers signed-in payment and guest payment
followed by password setup. It expects a real sandbox checkout, signed paid
order processing, persisted authenticated lifetime access and protected
HTML and Markdown, and checks the Polar billing summary. Signing out must
deny protected content. It uses Polar's sandbox card 4242 4242 4242 4242.
The subscription suite runs Pro monthly and Pro yearly purchases with fresh
signed-in buyers. It checks paid order fulfillment, both authenticated
subscription readers, the owned Polar portal and protected HTML/Markdown.
A recurring payment must not create lifetime access.
To exercise a discount, create a percentage discount restricted to your test
products with polar discounts create --help, then set its code in
POLAR_TEST_DISCOUNT_CODE before running the same suite. A 100% discount on
an approved fixed offer completes without card inputs. Set
POLAR_TEST_BILLING_COUNTRY="United Kingdom" for a separate tax-inclusive
checkout. The default address is in the United States. These settings belong
to the local test runner; they do not change the application's offer.
For SQL concurrency, immutable ownership, RLS and retained-migration proof against your disposable local Supabase instance, run:
bun --env-file=.env.local test ./e2e/integration/polar-repository.test.ts
bun --env-file=.env.local test ./e2e/integration/polar-subscription-repository.test.tsThis uses real local PostgreSQL with owned test rows and cleanup. It does not make a provider payment or replace the hosted-checkout suite.
Generated events are a separate ingress check:
polar webhook trigger order.paid --seed 7 --dry-run --json
polar webhook trigger order.paid --seed 7 --jsonGenerated IDs create no provider order or local checkout binding, so a
generated paid event must not grant access. A signed fixture does not prove
payment. Use polar checkouts get <checkout-id> and
polar orders get <order-id> for provider readback after a real checkout.
Purchase and refund behavior
/api/webhooks/polar verifies the raw delivery signature. Only order.paid
can establish the initial purchase, after Order and Checkout readback agree
with the application's saved offer and buyer. A redirect, metadata or
order.created does not grant access. Guests use the existing Supabase account
setup and onboarding email path.
Lifetime access has the same refund policy as the existing Stripe integration: partial and full refunds do not automatically remove application access. Refund notifications do not create a purchase. A refund from Polar's dashboard may revoke Polar-hosted benefits separately; those are not this application's persisted access authority. Review this policy against your own product terms.
Changing PAYMENT_PROVIDER affects new checkout. Keep the previous
provider's webhook endpoint and credentials configured for outstanding payments
and retries. Persisted Stripe lifetime purchases and Polar purchases in the
current POLAR_ENVIRONMENT continue to grant access.
Subscription access and customer management
A Polar subscription grants access while its current status is active and its verified funded interval contains the current time. A verified current trial can grant access until its trial end. Scheduled or actual termination caps either path; past-due, unpaid and terminal states deny access.
Each settled recurring order must identify the owned subscription/customer, approved fixed offer, currency and line service interval. An advanced provider billing period or a cycle-created notification alone does not extend access. Paid zero-value trial orders have no fixed-price line and do not establish funded coverage. An approved discounted fixed offer remains a separate case.
Plan and cadence changes among configured recurring offers use the actual settled replacement line, including a credit-funded annual-to-monthly change that shortens the funded interval. Older deliveries cannot restore superseded coverage. Bounded recovery can apply verified contiguous paid periods; a future, gapped or ambiguous interval preserves existing valid coverage and marks the subscription for reconciliation. There is no automatic unpaid grace period.
GET /api/user/status includes stored Stripe subscriptions and Polar
subscriptions from the current POLAR_ENVIRONMENT, including ineligible states.
Its managementTargets use the same Polar environment boundary; its singular
subscription remains the first eligible relationship for compatibility.
/api/user/subscription uses the same eligibility decision. The billing page
lists those subscriptions and provider-labelled management actions even after
paid access ends. Switching POLAR_ENVIRONMENT excludes the other environment's
access and management targets while retaining its history. PAYMENT_PROVIDER
selects new checkout independently of these readers. Stripe subscription actions
resolve the owned subscription's actual Stripe customer, so a later customer
snapshot cannot send the buyer to another billing account.
The browser posts an owned target from managementTargets to
/api/billing/portal. The server authenticates the buyer and selects the provider
customer. It returns only the destination URL, with a fixed application return
path. Raw customer IDs, redirect parameters, email and metadata do not authorize
portal access or establish purchase ownership.
Before taking live payments
Sandbox proof does not establish seller approval or live readiness. Review
your provider account, product, privacy and buyer disclosures, refund policy,
receipt/support responsibilities and hosted webhook configuration. A hosted
Polar endpoint must subscribe to order.paid, order.refunded,
subscription.created, subscription.updated, subscription.active,
subscription.cycled, subscription.canceled, subscription.uncanceled,
subscription.revoked, subscription.past_due, subscription.paused and
subscription.resumed. Live setup
uses separate production credentials and product IDs, with
POLAR_ENVIRONMENT=production. Do not use sandbox cards there.
Limit the production application's token to checkouts:write, products:read,
orders:read, subscriptions:read and customer_sessions:write.
Catalog administration, subscription updates, discounts, refunds and webhook
management are operator tasks; their development permissions do not belong in
the deployed application's token.
The current prod e2e billing:lifetime and prod e2e billing:subscription lanes remain Stripe-specific. Run the
documented local Polar sandbox journey and verify any later hosted Polar
deployment with hosted Polar setup
and the matching manual hosted billing checks. Real email delivery, refund
processing and provider recovery require their own observed checks.
See Polar sandbox limitations, webhook delivery and refunds. For Stripe configuration, continue with Payments.
Was this page helpful?