Read our blog posts.
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:
Before diving into code, here's why Polar stands out:
npm install @polar-sh/sdk @polar-sh/nextjs
You'll also need:
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);
}
}
prices field allows you to set dynamic prices per checkoutNow 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 };
}
}
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" };
}
}
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`
});
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;
}
},
});
subscription.created: New subscription startedsubscription.updated: Subscription modified (price change, etc.)subscription.canceled: User canceledsubscription.revoked: Admin revokedFinally, 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>
);
}
const metadata = checkout.metadata as unknown as YourMetadataType;
if (!metadata || !metadata.userId) {
throw new Error("Invalid checkout metadata");
}
Store the checkoutId or subscriptionId in your database to prevent duplicate provisioning.
Always test in sandbox before going live. Polar's sandbox mode is perfect for development.
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.