Skip to main content

@polar-sh/better-auth

A Better Auth plugin for integrating Polar payments and subscriptions into your authentication flow.

Features

Examples

Installation

Install the required Better Auth and Polar packages using the following command:
Terminal
The plugin requires Better Auth 1.7 or later and Node.js 22 or later.

Integrate Polar with BetterAuth

1

Configure Polar Access Token

Go to your Polar Organization Settings, create an Organization Access Token, and add it to the environment variables of your application.
.env
2

Configure BetterAuth Server

The Polar plugin comes with it’s own set of plugins to add functionality to your stack:
  • checkout - Enable seamless checkout integration
  • portal - Make it possible for your customers to manage their orders, subscriptions & benefits
  • usage - List customer meters & ingest events for Usage Based Billing
  • webhooks - Listen for relevant Polar webhooks
auth.ts

Polar Plugin Configuration Options

  • client (required): Polar core client, created with createPolarCore from @polar-sh/sdk/2026-10
  • use (required): Array of Polar plugins to enable specific functionality (checkout, portal, usage, and webhooks). Pass at least one plugin.
  • createCustomerOnSignUp (optional): Automatically create a Polar customer when a user signs up
  • getCustomerCreateParams (optional): Async function that returns additional metadata for new customers
  • experimental_organizationSync (optional): Mirror Better Auth organizations to Polar team customers. See Organization billing.
The core client has no service properties such as customers or events. To call the Polar API yourself, import the operation you need and pass the client to it:
Responses and webhook payloads use the SDK’s snake_case field names.
3

Configure BetterAuth Client

You will be using the BetterAuth Client to interact with the Polar functionalities.
auth-client.ts
organizationClient() is only needed if you use Better Auth’s organization plugin.

Automatic Customer creation on signup

Enable the createCustomerOnSignUp Polar plugin configuration option to automatically create a new Polar Customer when a new User is added in the BetterAuth database. Customer creation runs after Better Auth inserts the user, so the initial Polar creation request includes external_id: user.id. Applications that reject user creation in a before database hook do not create a Polar customer. getCustomerCreateParams runs only when a new customer is needed and receives the persisted user. It can add metadata; the user ID, email, and name remain authoritative. An existing customer with the same email and no external ID is linked once. An existing customer linked to the same user is reused; a different external ID produces a conflict without reassigning the customer. When a user’s email or name changes in Better Auth, the plugin updates the Polar customer. Anonymous users are skipped.

Sync Customer deletion

With createCustomerOnSignUp enabled, the plugin also deletes the Polar customer whose external ID matches the user when that user is deleted in Better Auth. You don’t need your own afterDelete hook.

Organization billing

The plugin supports two ways to bill Better Auth organizations.

Metadata-based billing with reference_id

Pass the organization ID as reference_id when creating a checkout. The checkout stays attached to the user’s personal Polar customer, and the ID is saved as metadata.referenceId on the checkout, order and subscription.
Checkout for an organization
List the subscriptions for that organization with the same reference_id:
List Organization Subscriptions Example
The plugin doesn’t check that the user belongs to the organization passed as reference_id, and the list includes every subscription in your Polar organization with that metadata.referenceId. Verify membership in your application before using the result to grant access.

Team customers with experimental_organizationSync

This integration is experimental. If your application already bills organizations another way, for example with reference_id, don’t enable it: existing subscriptions, orders, benefits and seats aren’t migrated, so Better Auth and Polar can end up inconsistent.
With Better Auth’s organization() plugin and experimental_organizationSync: { enabled: true }, each Better Auth organization is mirrored to a Polar team customer. The organization ID becomes the team customer’s external ID, and members are mirrored as Polar members with their user ID as external ID.
auth.ts
experimental_organizationSync accepts:
  • enabled (required): Turn synchronization on
  • getTeamCustomerCreateParams (optional): Add metadata, billing details or other fields to new team customers
  • mapBetterAuthRoleToPolarRole (optional): Map non-owner Better Auth roles to member or billing_manager
  • syncSeats (optional): Size and assign seat-based subscriptions from the Better Auth roster. Requires the webhooks plugin and the subscription.created and subscription.active events.
  • selectSeatProductsForMember (optional): Choose which seat products each member receives when syncSeats is enabled
Organization billing is always explicit. Pass organization_id to authClient.checkout(), organizationId to authClient.usage.ingest(), and query: { organizationId } to the portal, state, benefits, subscriptions, orders and meters methods. The plugin verifies the user’s membership before calling Polar, and checkout also requires a billing-capable role. The access token needs the customers:read, customers:write, members:read and members:write scopes. See the package README for the full role mapping and seat management behaviour.

Checkout Plugin

Source code To support checkouts in your app, you would pass the checkout plugin in the use property. The checkout plugin accepts the following configuration options:
  • products (optional): An array of product mappings or a function that returns them asynchronously. Each mapping contains a productId and a slug that allows you to reference products by a friendly slug instead of their full ID.
  • successUrl (optional): The relative path or absolute URL where customers will be redirected after a successful checkout completion. Relative paths resolve against your auth server URL. You can use the {CHECKOUT_ID} placeholder in the URL to include the checkout session ID in the redirect.
  • returnUrl (optional): An optional relative path or absolute URL which renders a back-button in the Checkout.
  • authenticatedUsersOnly (optional): When true, checkouts are rejected for signed-out and anonymous users. When false, anonymous checkouts are allowed. Either way, a signed-in user is attached to the checkout as the customer.
  • theme (optional): A string that can be used to enforce the theme of the checkout page. Can be either light or dark.
1

Use Checkout Plugin

Update the use property of the Polar plugin on your BetterAuth server to include the checkout plugin.
Checkout Plugin Example
2

Create checkouts using BetterAuth client

When the checkout plugin is passed, you are then able to initialize Checkout Sessions using the checkout method on the BetterAuth client. This will redirect the user to the product’s checkout link.
BetterAuth Checkout with Polar Example
The checkout method accepts the following snake_case properties:
  • products (optional): A Polar Product ID or an array of them
  • slug (optional): A string that can be used as a reference to the products defined in the Checkout config
  • reference_id (optional): An identifier saved as referenceId in the metadata of the checkout, order & subscription object. See Organization billing.
  • organization_id (optional): Bill a synchronized team customer. See Organization billing.
  • metadata, custom_field_data (optional): Metadata and custom field values for the checkout
  • allow_discount_codes (optional, defaults to true), discount_id, discount_code (optional): Discount settings. discount_id takes precedence when both are supplied.
  • seats, min_seats, max_seats (optional): Seat-based pricing settings
  • allow_trial, trial_interval, trial_interval_count (optional): Trial settings
  • success_url, return_url (optional): Override the plugin’s successUrl and returnUrl
  • redirect (optional, defaults to true): Set to false to get the checkout url back without redirecting
3

Embed the checkout (optional)

Use checkoutEmbed to open the checkout as an embed on your site instead of redirecting. It accepts the same properties as checkout.
BetterAuth Checkout Embed Example

Usage Plugin

Source code A plugin for Usage Based Billing that allows you to ingest events from your application and list the authenticated user’s Usage Meter. To enable usage based billing in your app, you would pass the usage plugin in the use property.
Usage Plugin Example

1. Event Ingestion

Polar’s Usage Based Billing builds entirely on event ingestion. Ingest events from your application, create Meters to represent that usage, and add metered prices to Products to charge for it.
Ingest events from your server, not from the browser. Events drive billing, so the client must never be trusted to decide what is consumed. Always ingest from the same server-side handler that performs the metered action (e.g. the route that generated the AI video, processed the upload, etc.) so that the recorded usage cannot be forged or bypassed.
Because createCustomerOnSignUp: true sets the Polar customer’s external ID to the BetterAuth user ID, you can pass external_customer_id: session.user.id when ingesting — no extra lookup is needed. The example below calls the Polar SDK directly from a server route handler, reusing the polarClient you configured in auth.ts:
app/api/ai/video/route.ts (Next.js App Router)
Each event accepts:
  • name (string): The name of the event to ingest. For example, ai_usage, video_streamed or file_uploaded.
  • external_customer_id (string): The BetterAuth user ID. Pass session.user.id from the server-side session.
  • metadata (object): A record of key-value pairs that describe the event. Values can be strings, numbers, or booleans. Use this to store information that can be filtered on or used to compute usage — duration, token count, file size, etc.

Client-side ingestion endpoint

The usage plugin also registers a BetterAuth endpoint that is callable from the client as authClient.usage.ingest(...). It forwards to Polar from the BetterAuth server and attaches the authenticated user automatically. Anonymous users are rejected:
auth-client.ts
This endpoint trusts whatever the client sends it, so a user can call it from their browser and claim any usage they want. Only use it for events that genuinely originate on the client and that you are comfortable not being authoritative (e.g. analytics-like signals). For billable usage, stick to server-side ingestion as shown above.

2. Customer Meters

A method to list the authenticated user’s Usage Meters (aka Customer Meters). A Customer Meter contains all the information about their consumption on your defined meters. The meters method of the usage plugin accepts the following parameters:
  • page (number): The page number for pagination (starts from 1).
  • limit (number): The maximum number of meters to return per page.
Customer Meters with Usage Plugin Example
The meters method returns the following fields in the response object:
  • Customer Information: Details about the authenticated customer
  • Meter Information: Configuration and settings of the usage meter
  • Customer Meter Information:
    • Consumed Units: Total units consumed by the customer
    • Credited Units: Total units credited to the customer
    • Balance: The balance of the meter, i.e. the difference between credited and consumed units.

Webhooks Plugin

Source code The webhooks plugin can be used to capture incoming events from your Polar organization. To set up the Polar webhooks plugin on your BetterAuth server, follow the steps below:
1

Configure Webhook Endpoints in Polar

Configure a Webhook endpoint in your Polar Organization Settings page by following this guide. The endpoint is served at /polar/webhooks under your Better Auth base path, which is /api/auth/polar/webhooks by default.
2

Add the Webhook Secret

Add the obtained webhook secret to your application environment as an environment variable (to be used as process.env.POLAR_WEBHOOK_SECRET):
.env
3

Use Webhooks Plugin in BetterAuth server

Pass the webhooks plugin in the use property.
Webhooks Plugin Example
The webhooks plugin allows you to invoke handlers for all Polar webhook events: Every handler is an async function that receives the full webhook payload ({ type, timestamp, data }). Fields use the SDK’s snake_case names, for example payload.data.customer_id.
  • onPayload - Called for every incoming webhook event, in addition to the matching handler below
  • onCheckoutCreated - Triggered when a checkout is created
  • onCheckoutExpired - Triggered when a checkout expires
  • onCheckoutUpdated - Triggered when a checkout is updated
  • onOrderCreated - Triggered when an order is created
  • onOrderUpdated - Triggered when an order is updated
  • onOrderPaid - Triggered when an order is paid
  • onOrderRefunded - Triggered when an order is refunded
  • onRefundCreated - Triggered when a refund is created
  • onRefundUpdated - Triggered when a refund is updated
  • onSubscriptionCreated - Triggered when a subscription is created
  • onSubscriptionUpdated - Triggered when a subscription is updated
  • onSubscriptionActive - Triggered when a subscription becomes active
  • onSubscriptionCanceled - Triggered when a subscription is canceled
  • onSubscriptionCycled - Triggered when a subscription enters a new billing period
  • onSubscriptionPastDue - Triggered when a subscription payment fails and it becomes past due
  • onSubscriptionPaused - Triggered when a subscription is paused
  • onSubscriptionResumed - Triggered when a paused subscription is resumed
  • onSubscriptionRevoked - Triggered when a subscription is revoked
  • onSubscriptionUncanceled - Triggered when a subscription cancellation is reversed
  • onProductCreated - Triggered when a product is created
  • onProductUpdated - Triggered when a product is updated
  • onOrganizationUpdated - Triggered when an organization is updated
  • onBenefitCreated - Triggered when a benefit is created
  • onBenefitUpdated - Triggered when a benefit is updated
  • onBenefitGrantCreated - Triggered when a benefit grant is created
  • onBenefitGrantCycled - Triggered when a benefit grant renews with its subscription
  • onBenefitGrantUpdated - Triggered when a benefit grant is updated
  • onBenefitGrantRevoked - Triggered when a benefit grant is revoked
  • onCustomerCreated - Triggered when a customer is created
  • onCustomerUpdated - Triggered when a customer is updated
  • onCustomerDeleted - Triggered when a customer is deleted
  • onCustomerStateChanged - Triggered when a customer state changes
  • onCustomerSeatAssigned - Triggered when a seat is assigned to a customer
  • onCustomerSeatClaimed - Triggered when a customer claims an assigned seat
  • onCustomerSeatRevoked - Triggered when a seat is revoked from a customer
  • onDiscountCreated - Triggered when a discount is created
  • onDiscountUpdated - Triggered when a discount is updated
  • onDiscountDeleted - Triggered when a discount is deleted
  • onMemberCreated - Triggered when a member is added to a team customer
  • onMemberUpdated - Triggered when a member of a team customer is updated
  • onMemberDeleted - Triggered when a member is removed from a team customer
The handler verifies the webhook-id, webhook-timestamp and webhook-signature headers against your webhook secret before calling any handler:
  • A missing or invalid signature returns 403.
  • A malformed payload returns 400.
  • A signed event type that the installed SDK doesn’t know yet returns 200 and is ignored, so new event types don’t cause retries.
  • If a handler throws, the error propagates and the request fails, so Polar retries the delivery.

Portal Plugin

Source code A plugin which enables customer management of their purchases, orders and subscriptions.
Portal Plugin Example
The portal plugin gives the BetterAuth Client a set of customer management methods, scoped under authClient.customer object. To act on a synchronized team customer instead of the user, pass query: { organizationId } to the list and state methods, or fetchOptions: { query: { organizationId } } to portal().

1. Customer Portal Management

The following method will redirect the user to the Polar Customer Portal, where they can see their orders, purchases, subscriptions, benefits, etc.
Open Customer Portal Example
Pass { redirect: false } to get the portal url back without redirecting. Anonymous users are rejected.

2. Customer State

The portal plugin also adds a convenient method to retrieve the Customer State.
Retrieve Customer State Example
The customer state object contains:
  • All the data about the customer.
  • The list of their active subscriptions.
  • The list of their granted benefits.
  • The list of their active meters, with their current balance.
Using the customer state object, you can determine whether to provision access for the user to your service. The user needs a Polar customer, so enable createCustomerOnSignUp or the request fails. Learn more about the Polar Customer State in the Polar Docs.

3. Benefits, Orders & Subscriptions

The portal plugin adds the following 3 convenient methods for listing benefits, orders & subscriptions relevant to the authenticated user/customer. Each returns a paginated list with items and pagination.

3.1 Benefits

This method only lists granted benefits for the authenticated user/customer.
List User Benefits Example

3.2 Orders

This method lists orders like purchases and subscription renewals for the authenticated user/customer.
List User Orders Example

3.3 Subscriptions

This method lists the subscriptions associated with authenticated user/customer.
List User Subscriptions Example
To list the subscriptions of an organization, pass reference_id or organizationId. See Organization billing. You can’t combine the two.