externalId
stores the immutable application user identifier (ID) on that customer. Email is
used only for a verified, one-time recovery association when an existing Polar
customer has no external ID. Do not apply this model unchanged to organizations,
teams, shared accounts, or seat-based billing.
The request path has four responsibilities:
- Better Auth: authenticates the request and proves email ownership before any email-based customer association.
- Your server: creates or associates the Polar customer and sets the allowed product, discounts, trials, customer IP address, and redirect URLs.
- Polar Customer State: returns the current customer and granted benefits.
- Your authorization code: checks one stable benefit ID before protected work runs.
- Pin the integration and local validation environment.
- Define the customer lifecycle and recurring product.
- Validate environment configuration.
- Add the Polar plugin to the existing Better Auth configuration.
- Create or safely associate a verified customer’s Polar record.
- Add bounded local rate limits and Customer State caching.
- Create checkout from a server-owned route.
- Authorize protected work from Customer State.
- Resolve checkout completion from verified server state.
- Add abortable, bounded client polling and portal recovery.
- Run build and sandbox validation before deployment.
Use the supported package set
The integration examples target exact Better Auth, Polar adapter, and Polar SDK versions. The optional validation harness is a disposable runnable application, not a production database or a replacement for your existing auth setup. For an existing application, keep its current Better Auth installation in place. The snippets on this page were validated with Better Auth1.6.23; do not
silently replace a working auth version just to follow the billing setup. Install
the missing integration dependencies with exact versions:
npm ls reports a Better Auth version other than 1.6.23, confirm adapter
compatibility and run the full validation path before deployment. Skip framework,
React, database, and TypeScript replacement when your application already
supplies them.
For the clean validation harness, use Node.js 22.19.0 with npm 10.9.3. Next.js
16 requires Node.js 20.9 or newer, and this harness keeps the runtime and Node type
declarations on the same major version.
1
Create the validation application
Create a TypeScript App Router application with
create-next-app 16.2.10.
The executable package version is available in the
create-next-app registry metadata.2
Pin the harness runtime and package manager
Record the exact local runtime, then make npm save exact dependency versions.
3
Install harness-only dependencies
Install the framework, integration, SQLite, and matching type packages used by
the runnable example.
1.8.4 declares @polar-sh/sdk ^0.47.0 and Better Auth ^1.4.12 in
its pinned
release manifest.
Keep the SDK on 0.47.x while using this adapter version. Also keep the canonical
Better Auth Polar plugin documentation
for supported usage and the pinned source only for version-specific behavior.
Choose the customer lifecycle before configuring Better Auth
This guide usescreateCustomerOnSignUp: false so signup does not depend on
Polar availability. Lazy creation also means the adapter’s customer create,
profile synchronization, and delete hooks are not your lifecycle policy.
With eager creation enabled, adapter 1.8.4 runs customer hooks around Better
Auth user operations. Those hooks are best-effort synchronization: they depend on
Better Auth operation context, and remote deletion can fail after the local user
has already been deleted. See the pinned
customer hooks.
Define these policies before deploying lazy creation:
- Email and name changes: update the Polar customer by its existing external ID after the local change is verified. Do not reassign ownership by email.
- Account deletion: decide whether to retain, delete, or anonymize the Polar customer. Deleting a customer can cancel subscriptions and revoke benefits; use the versioned Delete Customer API only when that matches your product policy.
- Account recreation: a new local user ID is a new identity. Do not let a new account take over a Polar customer whose external ID points to the deleted ID. Reconcile the record through an authenticated support or administrative flow.
- Account merge: choose the surviving local user ID first, then reconcile the Polar customer deliberately. Never infer a merge only from matching email.
Prepare Polar and the application
Create a recurring subscription product for the paid plan, not a one-time purchase. A Feature Flag benefit attached to a recurring product follows the subscription lifecycle, while a one-time purchase can grant the benefit permanently. See Feature Flag benefits.1
Create the recurring product and access benefit
Create the recurring Polar product, then attach one Feature Flag benefit for the
application capability this guide calls Pro access. Copy both IDs.
2
Create a scoped organization token
Create a Polar
Organization Access Token and
grant only the scopes used here:
customers:readcustomers:writecheckouts:readcheckouts:writecustomer_sessions:write
3
Keep verified-email signup controls enabled
The email recovery association later in this guide is safe only after the
application has proved ownership of the session email. Keep signup rate limits,
bot controls, and email verification enabled.For email and password, configure
emailVerification.sendVerificationEmail and
emailAndPassword.requireEmailVerification: true. For social sign-in, enable the
provider-specific requireEmailVerification option only for providers that
report a trustworthy verification claim. Otherwise, require a separate verified
email step before billing.APP_URL directly to
Better Auth instead of maintaining a second BETTER_AUTH_URL value.
CUSTOMER_IP_HEADER is empty in the local sandbox. In production, set it to the
header your trusted ingress overwrites with the connecting customer IP. Do not
trust a client-controlled forwarded header. Polar uses the request IP for country,
currency, billing-country prefilling, and preliminary tax behavior when checkout
is created from a backend.
Validate configuration at startup
Validate configuration once so route code uses typed values. The shared universally unique identifier (UUID) schema also validates checkout IDs later. Production requires HTTPS and a configured trusted customer-IP header.undefined.
Configure Better Auth and Polar
Add the Polar plugin to your existing Better Auth configuration without replacing its database adapter, auth methods, session policy, trusted origins, providers, or hooks. The full SQLite configuration below exists only for the clean harness. Create one Polar client for server code. Global retries remain disabled so write requests are never replayed automatically.plugins array.
polarBilling to the existing plugins array
and keep the rest of your auth configuration unchanged. Also ensure every auth
method that can reach billing produces a verified local email before association.
The clean harness uses SQLite and logs verification links only in sandbox. Never
use this database or console delivery as a production starting point.
latest CLI for a
schema migration.
auth.db is local-only. A relative SQLite file is not suitable for typical
serverless, ephemeral, or multi-process deployment, and it can contain user and
session data after testing. Production must keep the application’s existing
production-grade database configuration.
Create or associate the customer on the first billing action
The first billing action must ensure the verified signed-in user maps to exactly one Polar customer. Email recovery is allowed only when the local email is verified and the matching Polar customer has no external ID. Import the generated SDK error classes from their pinned model paths. These deep imports target SDK0.47.1, so re-run validation before changing the SDK.
422 does not
become success by itself: the helper performs one read-after-write recovery lookup
and rethrows the original validation error unless that lookup proves the customer
is now safely owned by this user. This bounds the concurrency recovery path
without treating arbitrary invalid data as a successful create.
Add bounded local API controls
The harness uses process-local controls so it can run without another service. Production with multiple processes must replace them with shared implementations. A distributed rate-limit consume/reset operation must be atomic; a remote read-then-write sequence is not sufficient. Use one bounded fixed-window helper for checkout, status polling, and protected feature routes. It removes expired entries during periodic sweeps and returns a retry delay forRetry-After.
Create checkout from a server-owned route
Keep price-sensitive fields on the server. The browser sends only the plan key; the server sets the product, discount policy, trial policy, customer identity, customer IP address, and redirect URLs. Define application-specific route constants once so examples cannot drift. Replace these values when your application uses different paths.Retry-After when it is exceeded.
Read Customer State and authorize on the server
Customer State is the authorization source. OnlyResourceNotFound from the
Customer State request means that no Polar customer exists; validation, timeout,
rate-limit, and server failures must propagate as availability failures.
Authorize the stable benefit ID attached to the recurring product.
Route checkout completion from verified state
The success URL never grants access. The server verifies the checkout ID, signed-in user, checkout ownership, checkout status, and Customer State before the browser can proceed. Define one client-safe runtime contract instead of duplicating a TypeScript type. There are five distinct response states:pending, ready, resume, failed,
and support.
confirmed only
means the customer clicked Pay; it does not prove payment success.
pending forever.
404, and returns 400 for malformed IDs.
Add an abortable finishing page
The browser polls only while the server returnspending. Every attempt has a
strict 30-second deadline, each fetch has an abort signal, cleanup cancels the
active loop, and retry cannot start a second concurrent loop.
The server component passes the checkout ID to the client component.
AbortController. Cleanup cancels the request and any sleep,
including React development Strict Mode cleanup. The retry button is disabled
while an attempt is active.
Add the customer portal and runnable harness pages
The Polar portal should be offered only after Customer State proves a Polar customer exists. The client must also surface portal failures instead of discarding them. Create the Better Auth client once for sign-in, signup, and Polar portal methods./sign-in.
Validate the integration
The clean harness is a runnable integration fixture, not proof that payment and security behavior is correct in your deployment. Run TypeScript checking, a full Next.js build, and sandbox tests that exercise auth, ownership, failure, and cancellation paths. Add a type-check script and run both static checks.- Use Node.js
22.19.0, npm10.9.3, and the exact dependency versions from a clean lockfile. - Run the pinned Better Auth migration and confirm
auth.dbremains ignored and local-only. - Sign up with email/password and confirm no authenticated session can use billing until the email is verified.
- For every enabled social provider, confirm the provider-specific verified email policy before allowing billing association.
- Start checkout and confirm extra browser fields cannot change product, discount, trial, redirect, customer identity, or customer IP policy.
- Confirm checkout receives the trusted customer IP in production-like proxy tests and rejects missing or malformed trusted ingress data.
- Confirm a new billing action creates one Polar customer with the local user
ID as
externalId. - Confirm a verified email-matched customer with no external ID associates once, while an unverified account cannot claim it.
- Reject a Polar customer whose external ID belongs to another local user.
- Verify local email/name changes use your explicit external-ID sync policy.
- Verify account deletion, recreation, and merge behavior matches the written lifecycle policy and never reclaims ownership by email alone.
- Reject cross-origin POST requests on checkout and every protected state-changing route.
- Return
401without a valid session,403for unverified billing identity, and429withRetry-Afterafter the documented route limits. - Confirm shared production rate limits use atomic consume/reset semantics and the production cache/circuit-breaker policy bounds organization API pressure.
- Return
400for malformed checkout IDs and404for missing or another user’s checkout. - Route
open,confirmed,succeeded,failed, andexpiredcheckout states as documented. - Grant access only when Customer State contains
POLAR_PRO_BENEFIT_ID. - After a succeeded checkout, confirm missing benefits change from bounded
pendingto the support/configuration state instead of polling forever. - Return
503when Polar cannot make an entitlement decision and distinguish durable401,403, or422upstream configuration failures in server logs. - Unmount and remount the finishing page under React development Strict Mode and confirm old fetches and sleeps are aborted.
- Confirm Try again is disabled during polling and cannot create overlapping attempts.
- Simulate network failure, fetch abort, proxy HTML, and invalid JSON; confirm checkout and polling controls recover with accessible errors.
- Confirm the finishing attempt stops within 30 seconds, including in-flight fetch and delay time.
- Hide Manage billing before a Polar customer exists and surface portal failures with a retryable client state.
- Run
npm run typecheckandnpm run buildwith every published TypeScript and TSX snippet in place.