Payments (Polar)
How payments and subscriptions are handled using Polar.
This document outlines the current approach to handling payments and subscriptions within the application, focusing on the primary integration with Polar.
Philosophy & Primary Provider: Polar
Currently, the main integrated payment provider is Polar. It offers a developer-focused platform that simplifies accepting payments and managing subscriptions.
Key Advantages of Polar:
- Merchant of Record: Polar acts as the Merchant of Record. They handle global sales tax, VAT, and compliance complexities. You receive consolidated payouts minus their fee, avoiding the need to manage tax collection yourself (unlike Stripe).
- Simplified Subscription Logic (Benefits): Polar uses "Benefits." Instead of checking complex subscription statuses, you grant a "Benefit" (e.g., "Pro Access") upon purchase. Your app logic only checks if the user has this Benefit. Polar automatically manages granting/revoking based on subscription status, refunds, etc., simplifying access control.
- Checking Benefit Status: The application checks if a user has the required benefit (e.g., the "Pro" benefit) using the
polar.getBillingState
tRPC procedure. This procedure fetches the user's granted benefits from Polar and compares them against thePOLAR_BENEFIT_PRO_ID_*
environment variables. - Benefit IDs: You define these Benefits in your Polar dashboard and configure their unique IDs in the
.env
file (POLAR_BENEFIT_PRO_ID_SANDBOX
andPOLAR_BENEFIT_PRO_ID_PROD
). - Automatic Revocation: Polar automatically revokes benefits if a subscription expires, is cancelled, or refunded, ensuring your application logic remains simple.
- Checking Benefit Status: The application checks if a user has the required benefit (e.g., the "Pro" benefit) using the
- Integrated Customer Portal: Polar provides a customer portal integrated with Better Auth (
/api/auth/portal
). Users manage subscriptions, plans, and payment methods on a Polar-hosted page. - Fast Setup: Integration typically takes only minutes by configuring environment variables.
Future Providers (Stripe)
Integration with Stripe might be added later, but the current focus is Polar.
Important: Enable Polar Early!
If you plan to charge users, enable the Polar integration and configure environment variables before users sign up.
createCustomerOnSignUp
Hook: Better Auth (src/server/auth/index.ts
) automatically creates a Polar customer record on signup if Polar is enabled. This links the user in your app to Polar.- Linking Purchases: Future purchases are automatically associated with this Polar customer.
- Problem with Late Setup: If a user signs up before Polar is enabled, no Polar customer record is created. Later purchases for this user will fail because there's no associated Polar customer.
- Current Mitigation: While migrating existing users might be possible later, the current flow depends on customer creation at signup.
Recommendation: Configure Polar from the start if you anticipate charging users to avoid issues.
Sandbox vs. Production Environments
Polar provides two distinct environments:
- Production: The live environment (
https://polar.sh/
). - Sandbox: A testing environment (
https://sandbox.polar.sh/
) that mirrors production functionality but uses test data.
Development Recommendation: Use Sandbox
It is highly recommended to use the Sandbox environment during local development.
- Isolation: Sandbox has its own separate projects, products, benefits, customers, and transactions. Actions in sandbox do not affect your live production data.
- Testing: You can safely test the entire checkout flow using fake card numbers (e.g.,
4242
repeated) provided by Polar/Stripe without incurring real charges.
Environment Switching (Local Development)
The application allows you to switch between Polar environments locally using the NEXT_PUBLIC_POLAR_ENV
environment variable (sandbox
or production
).
⚠️ Important Caveat: While you can test the production environment locally, be aware of how customer linking works. When a user signs up, their polarCustomerId
(stored in your database via the better-auth
plugin) is tied to the environment active at the time of signup.
- Example: If a user signs up while
NEXT_PUBLIC_POLAR_ENV=sandbox
, theirpolarCustomerId
points to a sandbox customer. If you then switchNEXT_PUBLIC_POLAR_ENV
toproduction
locally and that same user tries to purchase, the checkout will likely fail because the storedpolarCustomerId
(from sandbox) does not exist in Polar's production environment. - Recommendation: For consistent testing, stick to the sandbox environment locally. If you need to test production flows, consider using separate user accounts for production testing or utilize discount codes (see below).
Deployment: Production vs. Staging Environments
When deploying your application:
- For your live, user-facing production deployment: It is strongly recommended to set
NEXT_PUBLIC_POLAR_ENV=production
and use your corresponding Production Access Token and Benefit ID. - For staging or testing deployments: You can technically use
NEXT_PUBLIC_POLAR_ENV=sandbox
with your Sandbox keys. This allows you to test against the sandbox environment in a deployed setting. The application does not enforce the environment based on deployment status.
Important: Regardless of the environment used in deployment (production or sandbox for staging), ensure you are using the matching set of keys (Access Token, Benefit ID) for that environment (production
or sandbox
) in your environment variables.
Obtaining Keys & IDs
You need to obtain separate keys and IDs for both the Sandbox and Production environments from their respective dashboards (sandbox.polar.sh
and polar.sh
).
- Benefit ID (
POLAR_BENEFIT_PRO_ID_SANDBOX
/POLAR_BENEFIT_PRO_ID_PROD
):- Go to the "Benefits" section in the relevant Polar dashboard.
- Create a new Benefit (e.g., name it "Pro Access").
- Copy the generated Benefit ID.
- Paste it into the corresponding variable in your
.env
file.
- Access Token (
POLAR_ACCESS_TOKEN_SANDBOX
/POLAR_ACCESS_TOKEN_PROD
):- Go to Settings -> Developer section in the relevant Polar dashboard.
- Click "New Token".
- Give it a descriptive name.
- Select the necessary scopes. For simplicity, you can often select all scopes, but review them to ensure you grant only necessary permissions.
- Copy the generated token.
- Paste it into the corresponding variable in your
.env
file. Keep this token secret!
Testing Production Checkouts
To test the production checkout flow without making real payments:
- Go to your Production Polar dashboard.
- Create a Discount code that provides a 100% discount.
- Use this discount code during the checkout process in your application (while configured with production keys).
Webhooks: Optional
Webhooks are generally optional for the core functionality in this template.
- Benefit System: Access control relies primarily on checking the user's granted "Benefits" via the Polar API (
polar.getBillingState
), not webhooks. - Use Cases: You might implement webhooks for advanced custom automation based on specific payment events (e.g., sending a custom email on subscription renewal, storing customers in the database, etc).
Technical Integration Details
Setup & Configuration
-
Environment Variables: Defined in
src/env/schemas/polar.ts
and set in.env
.- Core:
NEXT_PUBLIC_ENABLE_POLAR
,NEXT_PUBLIC_POLAR_ENV
,POLAR_ACCESS_TOKEN_*
,POLAR_WEBHOOK_SECRET_*
,POLAR_BENEFIT_PRO_ID_*
. - Optional:
POLAR_CREATE_CUSTOMER_ON_SIGNUP
,POLAR_ENABLE_CUSTOMER_PORTAL
,POLAR_ENABLE_CHECKOUT
.
- Core:
-
Better Auth Plugin (
src/server/auth/index.ts
): The@polar-sh/better-auth
plugin is added if Polar is enabled.- Initializes the
@polar-sh/sdk
client. - Handles customer creation, customer portal route (
/api/auth/portal
), checkout flow, webhook handling (basic), and the state endpoint (/api/auth/state
). - Uses product IDs defined in
src/lib/payment-utils.ts
(getPlansForPolarPlugin
). - Redirects to
/checkout-success?checkout_id={CHECKOUT_ID}
on success.
- Initializes the
Fetching Billing State (tRPC)
- Router (
src/server/api/routers/polar.ts
): Exposespolar.getBillingState
. getBillingState
Procedure: Fetches data from/api/auth/state
, parses it (using Zod schemas), checks for the configured "Pro" benefit ID, and returns the state including anisPro
flag.
Configuring Payment Plans
The application comes with a default set of payment plans defined in src/config/payment-plans.ts
:
- Pro Monthly: A monthly subscription (
pro-monthly
). - Pro Annual: An annual subscription (
pro-annual
). - Lifetime Access: A one-time payment (
lifetime
).
You will need to configure the specific Product IDs from your Polar dashboard for these plans.
-
Create Products in Polar: Go to your Polar dashboard (both
sandbox.polar.sh
andpolar.sh
) and create the corresponding Products for each plan you intend to offer (e.g., a monthly subscription product, an annual subscription product, a one-time payment product). -
Copy Product IDs: For each product you create in Polar, copy its unique Product ID.
-
Edit
src/config/payment-plans.ts
: Open this file and paste the corresponding Product IDs into theproductIdsSandbox
andproductIdsProduction
objects:
Why Edit the File Directly?
Product IDs are configured directly in the code rather than via environment variables because:
- Flexibility: Most applications will have unique pricing structures (e.g., different tiers like Team, Enterprise, variations). Managing potentially many Product IDs via environment variables would be cumbersome.
- Clarity: Defining the plan structure and linking IDs directly in the code keeps the configuration clear and co-located with the plan definitions (
basePlans
).
Feel free to modify the basePlans
array and the corresponding Product ID mappings in src/config/payment-plans.ts
to match your specific product offerings.