externalId stores your application’s user identifier (ID) on the Polar customer.
This page implements one billing model: one Better Auth user maps to one Polar customer. Do not apply it unchanged to organizations, teams, or seat-based billing.
The request path has four responsibilities:
- Better Auth: authenticates the request and supplies the verified user ID
- Your server: creates or associates the Polar customer and fixes checkout policy
- Polar Customer State: returns active subscriptions and granted benefits
- Your authorization code: checks one stable benefit ID before protected work runs
Use the supported package set
This how-to pins the software development kit (SDK) and adapter versions that its imports target. The clean validation path also pins the framework and TypeScript toolchain.1
Create a clean validation application
Create a TypeScript App Router application with
create-next-app 16.2.10. This exact version is available in the versioned Next.js package metadata. Skip this step when you are adding the integration to an existing application.2
Install the pinned integration dependencies
Install the exact versions used by every snippet on this page. The clean harness also pins React and TypeScript so another reader can reproduce the type check.
1.8.4 declares @polar-sh/sdk ^0.47.0 and Better Auth ^1.4.12 in its release manifest. Keep the SDK on 0.47.x while you use this adapter version. The adapter 1.8.4 release includes the update to SDK 0.47.0.
Choose lazy customer creation before configuring Better Auth
This how-to setscreateCustomerOnSignUp: false and creates the Polar customer on the first billing action. That policy keeps Polar availability out of signup and makes recovery explicit.
Adapter 1.8.4 uses two eager hooks when createCustomerOnSignUp is true. It creates or finds a customer before local user creation, then assigns externalId after local creation. See the pinned customer hooks.
The same adapter also registers a user-deletion hook in the pinned server integration. With eager creation enabled, that hook deletes the matching Polar customer after local deletion. Polar customer deletion immediately cancels active subscriptions, revokes benefits, and clears the external ID. Polar anonymizes personally identifiable information only when deletion uses anonymize=true.
Because this page uses lazy creation, the adapter does not perform that automatic customer deletion. Define your account-deletion policy separately before adding remote deletion to your application.
Prepare Polar and the application
Complete these prerequisites before you add application code. Use separate Polar sandbox and production resources so IDs cannot cross environments.1
Create the product and access benefit
Create a Polar product for the paid plan. Then create a Feature Flag benefit, which grants application feature access, and attach it to that product.
2
Create a scoped organization token
Create a Polar Organization Access Token, which authenticates your server to the Polar application programming interface (API). Grant only the scopes used by this page:
customers:readcustomers:writecheckouts:readcheckouts:writecustomer_sessions:write
3
Protect signup before billing integration
Keep email verification, signup rate limits, and your bot challenge enabled. Lazy creation prevents Polar calls during signup, but these controls still protect account creation.
4
Add local environment variables
Set an explicit billing environment. Preview and staging deployments can set
NODE_ENV=production, so do not use NODE_ENV as the Polar environment selector.APP_URL. The webhook secret is absent because this how-to does not configure webhooks.
Validate configuration at startup
Validate billing configuration once so route code can use typed values. The shared UUID schema also validates checkout identifiers later in the flow.APP_URL to be an origin without a path or query.
Configure Better Auth and Polar
Create one Polar client for server code. Global retries stay disabled so write requests are never replayed automatically.1.8.4 parses the portal return URL during initialization, so pass an absolute URL. The behavior is visible in the pinned portal source.
Create or associate the customer on the first billing action
The first server-owned billing action must ensure that the signed-in user has one Polar customer. The helper below also recovers a customer that exists by email but lacks an external ID. Import the generated error classes from their model paths. These paths target the exact SDK0.47.1 package, so re-run the validation harness before changing the SDK.
Add a concrete checkout rate limiter
The local harness uses a fixed-window in-memory limiter so the route is runnable without another service. Replace this file with a shared-store implementation before deploying more than one application process.Create checkout from a server-owned route
Keep price-sensitive fields on the server. The browser sends only the plan key, while the server sets the product, discount policy, trial policy, and redirect URLs.- verifies the request origin for cross-site request forgery (CSRF) protection
- verifies the Better Auth session
- accepts only the
proplan key - rate limits each authenticated user
plan.
Read Customer State and authorize on the server
Customer State returns the current subscriptions and granted benefits for one customer. Treat only a real404 as “no Polar customer”; keep validation, rate-limit, timeout, and server failures distinct.
503 when Polar cannot answer instead of converting an availability failure into a denied entitlement.
Route checkout completion from verified state
The success URL does not grant access. The application must verify the checkout identifier, the signed-in user, checkout ownership, checkout status, and Customer State. The status endpoint returns one of four explicit results:
Define the response type next to the resolver so the browser and server share one contract.
confirmed, but only succeeded confirms checkout completion.
404 for a missing checkout or an ownership mismatch, and 503 for other Polar failures.
Add a bounded finishing page
The finishing page polls only while the status ispending. It stops after 30s and lets the customer retry or return to billing settings.
Add the customer portal client
The portal plugin adds authenticated customer methods to the Better Auth client. This client lets a customer open Polar’s hosted billing portal after a Polar customer exists.Validate the integration
The pinned setup at the start of this page is the local type-check harness. Copy every snippet into its documented file path, add the environment variables, and add this script topackage.json.
- Install the pinned Better Auth, adapter, and SDK versions from a clean lockfile
- Run the Better Auth database migration and start the Next.js application
- Sign in, start checkout, and verify the server ignores extra browser fields
- Confirm a new billing action creates one customer with the local user ID as
externalId - Confirm an email-matched customer with no external ID becomes associated once
- Reject a customer whose existing external ID belongs to another local user
- Return
401without a valid session and429after five checkout attempts per minute - Reject malformed checkout IDs and another user’s checkout with
404 - Route
open,confirmed,succeeded,failed, andexpiredcheckout states as documented - Grant protected access only after Customer State contains
POLAR_PRO_BENEFIT_ID - Return
503when Polar cannot provide the entitlement decision - Verify the 30s finishing flow stops polling and offers a retry path
- Run
npm run typecheckwith every published TypeScript and TSX snippet in place