Documentation

Payments (Polar)

Polar billing in Motoko Base — setup, checkout, portal, webhooks, and entitlements.

Open inChatGPT (opens in a new tab)Claude (opens in a new tab)Cursor (opens in a new tab)Polar billing in Motoko Base — setup, checkout, portal, webhooks, and entitlements.

Motoko Base uses Polar for subscriptions in v1. Billing is wrapped in a provider-agnostic API — feature code never imports Polar directly.

Official docs: Polar documentation

Architecture

Polar (source of truth)
   ↓
polar.customers.getStateExternal(userId)
   ↓
BillingSummary (plan, status, period end)
   ↓
Dashboard UI + hasPlan() / canAccess()

Polar is the source of truth. Motoko Base does not mirror subscriptions in Postgres. Plan state is fetched from Polar Customer State and cached briefly (60s). Webhooks invalidate the cache when subscriptions change.

Better Auth user id = Polar externalId. A Polar customer is created on sign-up when billing is configured.

Setup

  1. Create a Polar organization — start in sandbox
  2. Create a Pro subscription product → copy product ID
  3. Create an Organization Access Token
  4. Add a webhook → {APP_URL}/api/billing/webhooks/polar
  5. Set env vars in .env:
VariablePurpose
BILLING_PROVIDERpolar (default)
POLAR_ACCESS_TOKENOrganization access token
POLAR_WEBHOOK_SECRETWebhook signing secret
POLAR_PRO_MONTHLY_PRODUCT_IDPro monthly product ID
POLAR_PRO_YEARLY_PRODUCT_IDPro yearly product ID (optional)
POLAR_SERVERsandbox or production

Without Polar env vars the app stays on Free — no crash, checkout disabled.

Checkout & portal

ActionAPIUI
Upgrade to Procheckout(userId, "pro")Billing page → Upgrade
Manage subscriptionopenPortal(userId)Billing page → Manage billing

Checkout redirects to Polar Checkout. Portal opens Polar Customer Portal (cancel, update payment method, invoices).

Webhooks

Route: src/app/api/billing/webhooks/polar/

Polar sends subscription events → signature verified → billing cache invalidated → analytics event captured. No local subscription ledger.

Subscribe to subscription events + customer.state_changed in Polar dashboard. Unhandled event types are ignored safely.

Entitlements

import { hasPlan, canAccess } from "@/lib/billing";

await hasPlan(userId, "pro");
await canAccess(userId, "ai"); // feature → plan map in billingConfig

Feature→plan mapping lives in src/lib/billing/config.ts. Demo features are not gated in v1 — call canAccess when you add paid gates.

Plan labels in the UI: src/lib/billing/config/plans.ts.

Sandbox testing

  1. Set POLAR_SERVER=sandbox and sandbox token/product ID
  2. Use Polar test cards in checkout — see Polar sandbox docs
  3. Confirm webhook delivery in Polar dashboard (use ngrok locally if needed)
  4. After checkout, open /dashboard/settings/administration/billing?checkout=success — plan should update

Switch to POLAR_SERVER=production with production credentials for live billing.

Where to change this

WhatWhere
Public billing APIsrc/lib/billing/index.ts
Plans & feature gatessrc/lib/billing/config.ts
Polar providersrc/lib/billing/providers/polar/
Webhook handlersrc/lib/billing/providers/polar/webhooks.ts
Billing UI & actionssrc/features/dashboard/actions/billing.ts, src/features/dashboard/components/settings/billing-*

Do not import @polar-sh/* from features or UI — use @/lib/billing only.


Environment Variables — all Polar env vars.