Claude Code for Polar: Open-Source Developer Sales Platform — Claude Skills 360 Blog
Blog / Backend / Claude Code for Polar: Open-Source Developer Sales Platform
Backend

Claude Code for Polar: Open-Source Developer Sales Platform

Published: May 22, 2027
Read time: 6 min read
By: Claude Skills 360

Polar is an open-source developer sales platform for monetizing libraries, SaaS, and APIs — new Polar({ accessToken }) creates the SDK. polar.checkouts.custom.create({ products: [{ productId, productPriceId }], customerId }) generates a checkout URL. polar.subscriptions.get(id) retrieves subscription state. Webhooks validate with validateEvent(payload, headers, secret) — events include subscription.created, subscription.updated, subscription.revoked, and order.created. Benefits are deliverable perks: github_repository stars, discord_role, file_download, license_keys, and custom. polar.customers.getExternal({ externalId: userId }) finds a Polar customer by your user ID. polar.customerSessions.create({ customerId }) generates a customer portal URL. Polar is self-hostable and has a hosted version at polar.sh. Claude Code generates Polar checkout flows, subscription webhooks, benefit provisioning, and customer portal integration.

CLAUDE.md for Polar

## Polar Stack
- Version: @polar-sh/sdk >= 0.26
- Init: const polar = new Polar({ accessToken: process.env.POLAR_ACCESS_TOKEN!, server: "production" })
- Checkout: const { data } = await polar.checkouts.custom.create({ products: [{ productId }], successUrl, customerEmail })
- Get customer: await polar.customers.getExternal({ externalId: userId, organizationId: ORG_ID })
- Subscription: await polar.subscriptions.get({ id: subscriptionId })
- Portal: const { data } = await polar.customerSessions.create({ customerId })
- Webhook: validateEvent(rawBody, headers, POLAR_WEBHOOK_SECRET) — throws on invalid

Polar Client

// lib/billing/polar.ts — Polar SDK utilities
import { Polar } from "@polar-sh/sdk"
import { validateEvent, WebhookVerificationError } from "@polar-sh/sdk/webhooks"
import { db } from "@/lib/db"
import { users, subscriptions } from "@/lib/db/schema"
import { eq } from "drizzle-orm"

export const polar = new Polar({
  accessToken: process.env.POLAR_ACCESS_TOKEN!,
  server: process.env.NODE_ENV === "production" ? "production" : "sandbox",
})

const ORG_ID = process.env.POLAR_ORG_ID!

// Plan product/price IDs from Polar dashboard
const PLAN_PRODUCTS: Record<string, { productId: string; priceId?: string }> = {
  pro_monthly: { productId: process.env.POLAR_PRO_MONTHLY_PRODUCT_ID! },
  pro_yearly: { productId: process.env.POLAR_PRO_YEARLY_PRODUCT_ID! },
  enterprise_monthly: { productId: process.env.POLAR_ENTERPRISE_MONTHLY_PRODUCT_ID! },
}

// ── Checkout ───────────────────────────────────────────────────────────────
export async function createPolarCheckout(params: {
  userId: string
  email: string
  plan: string
  successUrl?: string
  metadata?: Record<string, string>
}): Promise<string> {
  const product = PLAN_PRODUCTS[params.plan]
  if (!product) throw new Error(`Unknown plan: ${params.plan}`)

  // Look up or note the Polar customer
  let polarCustomerId: string | undefined
  try {
    const customer = await polar.customers.getExternal({
      externalId: params.userId,
      organizationId: ORG_ID,
    })
    polarCustomerId = customer.id
  } catch {
    // Customer doesn't exist yet — Polar will create them at checkout
  }

  const checkout = await polar.checkouts.custom.create({
    products: [{ productId: product.productId }],
    successUrl: params.successUrl ?? `${process.env.APP_URL}/dashboard?welcome=1`,
    customerEmail: params.email,
    customerId: polarCustomerId,
    metadata: {
      userId: params.userId,
      ...params.metadata,
    },
  })

  return checkout.url
}

// ── Customer portal ────────────────────────────────────────────────────────
export async function createPortalSession(userId: string): Promise<string> {
  const customer = await polar.customers.getExternal({
    externalId: userId,
    organizationId: ORG_ID,
  })

  const session = await polar.customerSessions.create({
    customerId: customer.id,
  })

  return session.customerPortalUrl
}

// ── Webhook signature verification ────────────────────────────────────────
export { validateEvent, WebhookVerificationError }

Webhook Handler

// app/api/webhooks/polar/route.ts — subscription lifecycle
import { NextRequest, NextResponse } from "next/server"
import { validateEvent, WebhookVerificationError } from "@polar-sh/sdk/webhooks"
import { polar } from "@/lib/billing/polar"
import { db } from "@/lib/db"
import { users } from "@/lib/db/schema"
import { eq } from "drizzle-orm"

export async function POST(request: NextRequest) {
  const rawBody = await request.text()
  const headers = {
    "webhook-id": request.headers.get("webhook-id") ?? "",
    "webhook-timestamp": request.headers.get("webhook-timestamp") ?? "",
    "webhook-signature": request.headers.get("webhook-signature") ?? "",
  }

  let event: ReturnType<typeof validateEvent>

  try {
    event = validateEvent(rawBody, headers, process.env.POLAR_WEBHOOK_SECRET!)
  } catch (err) {
    if (err instanceof WebhookVerificationError) {
      return new Response("Invalid signature", { status: 403 })
    }
    return new Response("Webhook error", { status: 400 })
  }

  // Idempotency check
  const processed = await db.query.polarEvents?.findFirst({
    where: eq((polarEvents as any).eventId, event.id),
  })
  if (processed) return NextResponse.json({ ok: true })

  try {
    switch (event.type) {
      case "subscription.created":
      case "subscription.updated": {
        const sub = event.data
        const userId = sub.metadata?.userId as string | undefined

        if (!userId) break

        // Determine plan from product
        const productId = sub.product.id
        let plan: "free" | "pro" | "enterprise" = "free"
        if (Object.values(process.env).includes(productId)) {
          // Map back from product ID to plan name
          const entry = Object.entries({
            POLAR_PRO_MONTHLY_PRODUCT_ID: "pro",
            POLAR_PRO_YEARLY_PRODUCT_ID: "pro",
            POLAR_ENTERPRISE_MONTHLY_PRODUCT_ID: "enterprise",
          }).find(([envKey]) => process.env[envKey] === productId)
          if (entry) plan = entry[1] as "pro" | "enterprise"
        }

        const activeStatuses = ["active", "trialing"]
        await db.update(users)
          .set({ plan: activeStatuses.includes(sub.status) ? plan : "free" })
          .where(eq(users.id, userId))
        break
      }

      case "subscription.revoked":
      case "subscription.canceled": {
        const sub = event.data
        const userId = sub.metadata?.userId as string | undefined
        if (!userId) break

        await db.update(users)
          .set({ plan: "free" })
          .where(eq(users.id, userId))
        break
      }

      case "order.created": {
        // One-time purchase — provision immediately
        const order = event.data
        const userId = order.metadata?.userId as string | undefined
        if (!userId) break

        // Grant access based on product
        console.log(`[Polar] order created for user ${userId}, product ${order.product.id}`)
        break
      }

      case "benefit.granted": {
        // A benefit was provisioned (GitHub repo access, Discord role, etc.)
        const { benefit, customer } = event.data
        console.log(`[Polar] benefit granted: ${benefit.type} for customer ${customer.id}`)
        break
      }
    }

    return NextResponse.json({ ok: true })
  } catch (err) {
    console.error("[Polar webhook error]", err)
    return NextResponse.json({ error: "Handler error" }, { status: 500 })
  }
}

Billing Status Hook

// hooks/usePolarBilling.ts — billing state + checkout
"use client"
import { useState, useCallback } from "react"

export function usePolarBilling() {
  const [isLoading, setIsLoading] = useState(false)

  const startCheckout = useCallback(async (plan: string) => {
    setIsLoading(true)
    try {
      const res = await fetch("/api/billing/checkout", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ plan }),
      })
      const { url } = await res.json()
      window.location.href = url
    } finally {
      setIsLoading(false)
    }
  }, [])

  const openPortal = useCallback(async () => {
    setIsLoading(true)
    try {
      const res = await fetch("/api/billing/portal")
      const { url } = await res.json()
      window.open(url, "_blank")
    } finally {
      setIsLoading(false)
    }
  }, [])

  return { startCheckout, openPortal, isLoading }
}

For the Stripe alternative when enterprise-grade billing features like complex subscription items, metered usage, detailed revenue reporting, advanced fraud detection, and a mature webhook ecosystem are required — Stripe is the industry standard for billing infrastructure while Polar is purpose-built for developer tools and OSS monetization with better defaults for that use case, see the Stripe Subscriptions guide. For the Lemon Squeezy alternative when a Merchant of Record solution that handles taxes globally without any tax registration is preferred — both Polar (in MoR mode) and Lemon Squeezy handle taxes, but Lemon Squeezy has a longer track record, see the Lemon Squeezy guide. The Claude Skills 360 bundle includes Polar skill sets covering checkout, webhooks, and benefits. Start with the free tier to try developer billing generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free