Prerequisites

  1. go to sandbox.polar.sh and create an organization.
  2. go to your organization and create a product.
  3. get the product id and set it in the environment variables.
  4. Required environment variables: in apps/server/.env
    POLAR_ACCESS_TOKEN=your_polar_access_token
    POLAR_WEBHOOK_SECRET=your_webhook_secret
    POLAR_SUCCESS_URL=your_success_url
    
    POLAR_PRODUCT1_PRODUCT_ID=your_product_id // this is the product id of the product you created in the dashboard
    

Installation

Install the required dependencies:
bun add @polar-sh/sdk @polar-sh/better-auth

Server-Side Configuration

  1. Initialize the Polar client: in apps/server/src/lib/polar.ts
import { Polar } from "@polar-sh/sdk";

export const polarClient = new Polar({
  accessToken: process.env.POLAR_ACCESS_TOKEN,
  server: "sandbox", // or "production"
});
  1. Configure BetterAuth with Polar plugin:
import { betterAuth } from "better-auth";
import { polar } from "@polar-sh/better-auth";

export const auth = betterAuth({
  plugins: [
    polar({
      client: polarClient,
      createCustomerOnSignUp: true,
      use: [
        checkout({
          products: products.map((product) => ({
            productId: product.productId,
            slug: product.slug,
          })),
          successUrl: `${process.env.POLAR_SUCCESS_URL}`,
          authenticatedUsersOnly: true,
        }),
        portal(),
        webhooks({
          secret: process.env.POLAR_WEBHOOK_SECRET,
          onPayload: async ({ data, type }) => {
            // Handle webhook events
          },
        }),
      ],
    }),
  ],
});

Webhook Handling

import { webhooks } from "@polar-sh/better-auth";

export const auth = betterAuth({
  plugins: [
    webhooks({
      secret: process.env.POLAR_WEBHOOK_SECRET ||
		(() => {
			throw new Error("POLAR_WEBHOOK_SECRET environment variable is required",);})(),
					onPayload: async ({ data, type }) => {
						if (
							type === "subscription.created" ||
							type === "subscription.active" ||
							type === "subscription.canceled" ||
							type === "subscription.revoked" ||
							type === "subscription.uncanceled" ||
							type === "subscription.updated"
						) {
							console.log(
								"🎯 Processing subscription webhook:",
								type,
							);
							console.log(
								"📦 Payload data:",
								JSON.stringify(data, null, 2),
							);

							try {
								const userId = data.customer?.externalId;

								const subscriptionData = {
									id: data.id,
									provider: Provider.POLAR,
									providerSubscriptionId: data.id,
									providerCustomerId: data.customerId,
									userId: userId as string,
									plan: products.find(
										(p) => p.productId === data.productId,
									)?.slug ?? "default",
									amount: data.amount,
									currency: data.currency,
									status: data.status,
									cancelAtPeriodEnd: data.cancelAtPeriodEnd,
									currentPeriodStart: new Date(
										data.currentPeriodStart,
									),
									currentPeriodEnd: data.currentPeriodEnd
										? new Date(data.currentPeriodEnd)
										: undefined,
									createdAt: new Date(data.createdAt),
									updatedAt: data.modifiedAt
										? new Date(data.modifiedAt)
										: undefined,
									metadata: data.metadata ?? {},
								};

								console.log("💾 Final subscription data:", {
									id: subscriptionData.id,
									status: subscriptionData.status,
									userId: subscriptionData.userId,
									amount: subscriptionData.amount,
								});

								console.log(
									"🔍 Checking for existing subscription:",
									data.id,
								);
								const existingSubscription = await prisma
									.subscription.findUnique({
										where: {
											id: data.id,
										},
									});

								if (existingSubscription) {
									console.log(
										"🔄 Updating existing subscription:",
										data.id,
									);
									await prisma.subscription.update({
										where: {
											id: data.id,
										},
										data: subscriptionData,
									});
								} else {
									console.log(
										"🆕 Creating new subscription:",
										data.id,
									);
									await prisma.subscription.create({
										data: subscriptionData,
									});
								}

								console.log(
									"✅ Subscription processed:",
									data.id,
								);
							} catch (error) {
								console.error(
									"💥 Error processing subscription webhook:",
									error,
								);
							}
						}
					},
				}),

Client-Side Configuration

Set up the auth client with Polar support:
import { createAuthClient } from "better-auth/react";
import { polarClient } from "@polar-sh/better-auth";

export const authClient = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_SERVER_URL!,
  plugins: [
    polarClient(),
  ],
});

Product Configuration

Define your products in a configuration file: in apps/web/src/src/products.ts
export const products = [
  {
    productId: process.env.POLAR_PRODUCT1_PRODUCT_ID,
    slug: "basic",
    name: "Basic Plan",
    price: "$10/month",
    features: ["Feature 1", "Feature 2"],
  },
  {
    productId: process.env.POLAR_PRODUCT2_PRODUCT_ID,
    slug: "pro",
    name: "Pro Plan",
    price: "$20/month",
    features: ["Feature 1", "Feature 2", "Feature 3"],
  },
];
in apps/server/src/utils/products-list.ts
export const products = [
  {
    productId: process.env.POLAR_PRODUCT1_PRODUCT_ID,
    slug: "basic",
    name: "Basic Plan",
    price: "$10/month",
    features: ["Feature 1", "Feature 2"],
  },
  {
    productId: process.env.POLAR_PRODUCT2_PRODUCT_ID,
    slug: "pro",
    name: "Pro Plan",
    price: "$20/month",
    features: ["Feature 1", "Feature 2", "Feature 3"],
  },
];

Webhook Handling

Set up webhook handling for Polar events:
// src/app/api/webhooks/polar/route.ts
import { polar } from "@/lib/polar/client";
import { headers } from "next/headers";

export async function POST(req: Request) {
  const body = await req.json();
  const signature = headers().get("polar-signature");

  try {
    const event = polar.webhooks.constructEvent(
      body,
      signature!,
      process.env.POLAR_WEBHOOK_SECRET!
    );

    switch (event.type) {
      case "subscription.created":
      case "subscription.active":
      case "subscription.canceled":
      case "subscription.revoked":
      case "subscription.uncanceled":
      case "subscription.updated":
        await handleSubscriptionEvent(event.data);
        break;
    }

    return new Response("Webhook processed", { status: 200 });
  } catch (error) {
    console.error("Webhook error:", error);
    return new Response("Webhook error", { status: 400 });
  }
}

Subscription Management

Starting a Checkout

await authClient.checkout({
  products: [productId],
  slug: productSlug,
});

Common Issues

  1. Webhook Signature Verification
    • Ensure the webhook secret is correctly set
    • Verify the signature in the webhook handler
  2. Subscription Status Sync
    • Implement proper webhook handling
    • Update the database on subscription changes
  3. Checkout Session
    • Set correct success and cancel URLs
    • Handle failed payments gracefully