Beta Launch: Early access to the platform. We're also building a complete suite of deployment products.

Blog

Read our blog posts.

How to Integrate Polar.sh with Next.js for Subscription Payments

If you're building a SaaS product with Next.js and need a developer-friendly payment solution, Polar.sh is a game-changer. Unlike Stripe, which requires extensive configuration, Polar offers a clean SDK, built-in webhook handling, and native support for custom pricing.

In this guide, we'll walk through a real-world implementation of Polar in a Next.js application, covering:

  1. Setting up the Polar SDK
  2. Creating checkout sessions with custom prices
  3. Handling webhooks for subscription lifecycle events
  4. Managing subscription metadata

Why Polar.sh?

Before diving into code, here's why Polar stands out:

Prerequisites

npm install @polar-sh/sdk @polar-sh/nextjs

You'll also need:

Step 1: Create a Polar Service (Singleton Pattern)

First, let's create a reusable service to interact with Polar. This singleton pattern ensures we only initialize the SDK once.

// lib/services/polar.service.ts
import { Polar } from "@polar-sh/sdk";
import type { CheckoutCreate } from "@polar-sh/sdk/models/components/checkoutcreate.js";

export class PolarService {
    private static instance: PolarService;
    private client: Polar;

    private constructor() {
        this.client = new Polar({
            accessToken: process.env.POLAR_ACCESS_TOKEN!,
            server: process.env.NODE_ENV === "production" ? "production" : "sandbox",
        });
    }

    public static getInstance(): PolarService {
        if (!PolarService.instance) {
            PolarService.instance = new PolarService();
        }
        return PolarService.instance;
    }

    public getClient(): Polar {
        return this.client;
    }

    /**
     * Create a checkout session with custom pricing
     */
    public async createCheckout(
        productId: string,
        priceAmountCents: number,
        metadata: Record<string, any>,
        successUrl: string,
        customerEmail: string,
        customerName: string
    ) {
        const checkoutConfig: CheckoutCreate = {
            products: [productId],
            metadata,
            successUrl,
            customerEmail,
            customerName,
            allowDiscountCodes: true,
        };

        // Only include custom prices for paid subscriptions
        if (priceAmountCents > 0) {
            checkoutConfig.prices = {
                [productId]: [{
                    amountType: "fixed",
                    priceAmount: priceAmountCents,
                    priceCurrency: "usd"
                }]
            };
        }

        return this.client.checkouts.create(checkoutConfig);
    }
}

Key Points:

Step 2: Create a Billing Service

Now let's build a higher-level service that handles pricing logic and checkout creation.

// lib/services/billing.service.ts
import { PolarService } from "./polar.service";

export class BillingService {
    private polarService: PolarService;

    constructor() {
        this.polarService = PolarService.getInstance();
    }

    /**
     * Create a subscription checkout with calculated pricing
     */
    public async createSubscriptionCheckout(
        config: any,
        successUrl: string,
        customerEmail: string,
        customerName: string,
        metadata: Record<string, any>
    ) {
        // Calculate price based on resources
        const price = this.calculatePrice(config);

        // Get product ID
        const productId = process.env.POLAR_PAID_PRODUCT_ID!;

        // Create checkout with custom price
        return this.polarService.createCheckout(
            productId,
            price.totalAmountCents,
            { ...metadata, ...config },
            successUrl,
            customerEmail,
            customerName
        );
    }

    private calculatePrice(config: any) {
        // TODO: Implement pricing logic
        return { totalAmountCents: 2500 };
    }
}

Step 3: Create Server Actions for Checkout

Next.js Server Actions make it easy to initiate checkouts from the client.

// lib/actions/payment.ts
"use server";

import { BillingService } from "@/lib/services/billing.service";

export async function initiatePodCheckout(values: any) {
    // Verify user session (your auth logic here)
    const session = await verifySession();
    
    if (!session.isAuth) {
        return { error: "Unauthorized" };
    }

    try {
        const billingService = new BillingService();

        const checkout = await billingService.createSubscriptionCheckout(
            values,
            `${process.env.NEXT_PUBLIC_URL}/confirmation?checkout_id={CHECKOUT_ID}`,
            session.user.email,
            session.user.name,
            {
                userId: session.user.id,
                podName: values.name,
            }
        );

        return { url: checkout.url };
    } catch (error) {
        console.error("Failed to create checkout:", error);
        return { error: "Failed to initiate checkout" };
    }
}

Step 4: Set Up the Checkout Route

Polar provides a Next.js helper for the checkout redirect flow.

// app/checkout/route.ts
import { Checkout } from "@polar-sh/nextjs";

export const GET = Checkout({
    accessToken: process.env.POLAR_ACCESS_TOKEN!,
    server: process.env.NODE_ENV === "production" ? "production" : "sandbox",
    successUrl: `${process.env.NEXT_PUBLIC_URL}/confirmation`
});

Step 5: Handle Webhooks

Webhooks are crucial for keeping your database in sync with Polar's subscription state.

// app/api/webhook/polar/route.ts
import { Webhooks } from "@polar-sh/nextjs";
import { db } from "@/lib/db";
import { subscriptions } from "@/lib/db/schema";

export const POST = Webhooks({
    webhookSecret: process.env.POLAR_WEBHOOK_SECRET!,
    onPayload: async (payload) => {
        switch (payload.type) {
            case "subscription.created":
            case "subscription.updated":
                const sub = payload.data;
                
                // TODO: Update subscription in database
                break;

            case "subscription.canceled":
            case "subscription.revoked":
                // TODO: Cancel subscription in database
                break;
        }
    },
});

Important Webhook Events:

Step 6: Client-Side Integration

Finally, trigger the checkout from your React component.

// components/checkout-button.tsx
"use client";

import { useState } from "react";
import { initiatePodCheckout } from "@/lib/actions/payment";

export function CheckoutButton({ config }: { config: ResourceConfig }) {
    const [loading, setLoading] = useState(false);

    const handleCheckout = async () => {
        setLoading(true);
        const result = await initiatePodCheckout(config);
        
        if (result.url) {
            window.location.href = result.url;
        } else {
            alert(result.error);
        }
        setLoading(false);
    };

    return (
        <button onClick={handleCheckout} disabled={loading}>
            {loading ? "Processing..." : "Subscribe Now"}
        </button>
    );
}

Best Practices

1. Always Validate Metadata

const metadata = checkout.metadata as unknown as YourMetadataType;
if (!metadata || !metadata.userId) {
    throw new Error("Invalid checkout metadata");
}

2. Use Idempotency

Store the checkoutId or subscriptionId in your database to prevent duplicate provisioning.

3. Test with Sandbox Mode

Always test in sandbox before going live. Polar's sandbox mode is perfect for development.

Conclusion

Polar.sh makes subscription billing in Next.js incredibly straightforward. With custom pricing support, built-in webhooks, and a clean SDK, you can focus on building your product instead of wrestling with payment infrastructure.

Ready to implement Polar in your Next.js app? Check out the official Polar documentation for more advanced features like usage-based billing and customer portals.

Travos.ai - Deploy n8n with Managed Infrastructure