This guide covers the common per-user SaaS billing path. If you bill organizations or seats withDocumentation Index
Fetch the complete documentation index at: https://tmbv.me/llms.txt
Use this file to discover all available pages before exploring further.
referenceId, the entity you authorize is different, and this page is not the right pattern.
The core model is:
- Better Auth owns the auth + billing glue: customer creation, checkout, portal methods, and webhook signature verification.
- Polar owns current billing truth: read Customer State when you need to know what the user should have access to.
- Your backend owns authorization: loaders, actions, API routes, and mutations should decide access on the server.
- Redis / KV is infrastructure, not truth: use it for sessions, rate limits, and optional cache layers, not as the canonical source of billing state.
- a separate
user -> polarCustomerIdmapping table - a shadow
subscriptionstable - a
priceIdcolumn onuser
createCustomerOnSignUp: true, the Better Auth / Polar integration associates the customer to your Better Auth user.id through Polar externalId, so you can read billing state directly by your own user ID.
Minimal Integration
1. Configure Better Auth on the server
- In steady state, the Polar customer is addressable by Better Auth
user.idthrough PolarexternalId. - Checkout sessions automatically bind to the signed-in user when
authenticatedUsersOnly: true. - The client gets portal methods under
authClient.customer.*. - Webhook signatures are verified before your handler runs.
- Sandbox and production are fully separate in Polar. Tokens, products, discounts, and customers do not cross environments.
- In the current adapter implementation, customer creation and
externalIdassignment happen in separate signup hooks. TreatexternalId = user.idas the resulting association, not as an atomic guarantee on the very first customer create call. - Assuming you mount Better Auth at the default
/api/auth/*, the current Polar BetterAuth adapter guide documents the webhook endpoint as/api/auth/polar/webhooks. If you customize the Better Auth base path, derive the webhook route from that mount. - Polar’s API docs use
snake_case, but the TypeScript SDK converts fields tocamelCase. That is why this page uses SDK names likeexternalId,grantedBenefits, andexternalCustomerId.
onCustomerStateChanged entirely.
2. Start checkout from the Better Auth client
authClient.checkout({ slug: 'pro' }) directly.
You can also open the hosted customer portal with authClient.customer.portal(), and you can fetch customer state on the client with authClient.customer.state() when you need to render plan badges or billing UI. Still, the frontend should never be the thing that finally decides access.
3. Read Customer State on the server
Model Access Around Benefits, Not “Any Subscription”
This is the biggest practical fix to the original write-up.activeSubscriptions.length > 0 is only safe if you truly mean “any active subscription at all grants the same paid access.” In many real apps that stops being true quickly:
- you may have multiple paid tiers
- you may have a free recurring plan
- you may have one-time purchases that should still grant access
- you may want access to follow benefits, not products
slug: pro on the benefit and match against that instead. Polar now includes benefit metadata in Customer State, so that works cleanly.
For coarse checks, matching activeSubscriptions[*].productId is also fine. For actual feature gating, benefits are the cleaner contract.
Keep /billing/success as UX, Not Authorization
- Confirm the checkout belongs to the signed-in user.
- Treat
confirmedas “payment flow still settling,” not as success. Terminal non-success states likeexpiredorfailedshould fall back to regular billing UX, not unlock access. - Redirect optimistically once Customer State actually shows the access you care about.
- It is not the final source of truth for gated routes.
- It is not a replacement for checking Customer State on the backend.
Where Upstash Redis Fits
If you already use Upstash Redis as Better AuthsecondaryStorage, that is a good place for:
- sessions
- verification records
- rate-limit counters
- optional short-lived billing caches
billing:catalogbilling:state:*rl:billing:*
- Polar Customer State is where you decide access.
- Redis is where you make that access check cheaper or faster.
Webhooks Are for Cache Invalidation and Side Effects
If you keep app-side cached billing state,customer.state_changed is the right event to refresh or invalidate it.
Do not rebuild billing truth from a pile of subscription.*, order.*, and benefit_* events when Polar already gives you Customer State directly.
A good webhook handler for this integration is:
- idempotent
- fast
- safe to retry
- mostly about cache invalidation, denormalized read models, and audit side effects
Sync Customer Deletion Explicitly
The current Polar BetterAuth adapter guide documents this pattern explicitly. If you let users delete their Better Auth account, explicitly decide what happens to the matching Polar customer.afterDelete hook pattern without anonymize; whether you set anonymize: true is your policy choice.
Two policy choices live here:
- whether deleting the app user should also delete the Polar customer immediately
- whether you want
anonymize: truefor PII handling, or whether your retention policy requires something else
What Your App Still Owns
- Benefit / entitlement mapping
- Server-side authorization checks
- Optional cache shape and invalidation policy
- Success-page UX
- Deletion and retention policy
- Monitoring and alerting
Pitfalls
- Do not rebuild billing truth from individual events when Polar already gives you Customer State.
- Do not use
activeSubscriptions.length > 0as a universal “paid user” check unless every qualifying plan really means the same access. - Do not treat
confirmedcheckout status as payment success; onlysucceededis final. - Do not hardcode a webhook URL without considering your Better Auth base path. For the default Better Auth mount, the current Polar BetterAuth adapter guide documents
/api/auth/polar/webhooks. - Do not read Redis billing keys as source-of-truth business state.
- Do not let the frontend be the final authority for access decisions.
- Do not use webhook timestamps as blanket replay rejection; legitimate retries and redeliveries happen.
- Do not put heavy work directly in the webhook request path.
- Do not assume deleting a customer anonymizes PII by default;
anonymize: trueis opt-in.
Further Reading
- Better Auth Polar plugin
- Polar BetterAuth adapter guide
- Polar Customer State guide
- Polar Feature Flag benefits
- Better Auth secondary storage
- Better Auth session management
- Better Auth rate limiting
- Polar webhook delivery guide
- Polar TypeScript SDK
- Get Customer State by External ID
- Get Checkout Session
- Delete Customer by External ID