From 87320a1a9299f1d605343659e4968ac02f9da8ea Mon Sep 17 00:00:00 2001 From: Krs Date: Mon, 24 Aug 2026 12:43:54 -0400 Subject: [PATCH] fix(billing): add customer portal helper, benefits access, and Polar scaffold docs (#2) --- _template_options/payments/_polar/README.md | 37 ++++++++++++++----- .../_polar/billing-adapter.ts.example | 22 +++++++++++ 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/_template_options/payments/_polar/README.md b/_template_options/payments/_polar/README.md index 3a47dd1..79293e1 100644 --- a/_template_options/payments/_polar/README.md +++ b/_template_options/payments/_polar/README.md @@ -2,30 +2,47 @@ Use this when the product should sell through Polar checkout sessions and later map orders, subscriptions, or benefits into app entitlements. +## Installation + Install only if selected: ```sh bun add @polar-sh/sdk ``` -Environment: +## Environment Variables + +Server-side only (`.env` / server runtime): ```env -POLAR_ACCESS_TOKEN= -POLAR_SERVER=sandbox -POLAR_PRODUCT_PRO= +POLAR_ACCESS_TOKEN=polar_at_... +POLAR_SERVER=sandbox # 'sandbox' | 'production' +POLAR_PRODUCT_PRO=prod_... ``` -Activation checklist: +## When to Choose Polar vs Autumn / Stripe Direct + +- **Choose Polar** when you want an open-source Merchant of Record (MoR) / developer-centric monetization platform that handles EU VAT / global sales tax, built-in customer portals, and automatic benefits distribution (e.g. GitHub repository access, Discord role grants, file downloads, license keys). +- **Choose Autumn** when you want lightweight usage-based metering tightly integrated with Convex and direct Stripe accounts. +- **Choose Stripe Direct** when you have custom Stripe Elements requirements, specific marketplace payout flows, or existing Stripe Connect infrastructure. + +## Customer Portal & Benefits Access + +- **Customer Portal**: Polar provides a hosted customer portal out of the box. Use `createCustomerPortalSession` to generate a direct management URL for customers to manage subscriptions, update payment methods, and download invoices. +- **Entitlements & Benefits**: Polar supports automated benefit grants (GitHub repository invites, Discord roles, digital file downloads). Entitlements can be queried via Polar API or synced via `order.created` / `subscription.active` webhooks. + +## Activation Checklist 1. Copy `billing-adapter.ts.example` to `src/lib/server/billing/provider.ts`. 2. Keep `POLAR_ACCESS_TOKEN` server-side only. -3. Create checkout sessions from server routes or Convex actions, never from client components. +3. Create checkout sessions and customer portal sessions from server routes or Convex actions, never from client components. 4. Store `external_customer_id` as the app user ID. -5. Add webhook handling before granting paid access. +5. Add webhook handling (`/api/webhooks/polar`) before granting paid access. 6. Remove Autumn or other billing providers after Polar checkout and entitlement sync work. -Official docs checked while creating this scaffold: +## References -- Polar API overview: https://polar.sh/docs/api-reference/introduction -- Polar create Checkout Session: https://polar.sh/docs/api-reference/checkouts/create-session +- Polar API Overview: https://polar.sh/docs/api-reference/introduction +- Polar Create Checkout Session: https://polar.sh/docs/api-reference/checkouts/create-session +- Polar Customer Portal: https://polar.sh/docs/api-reference/customer-portal +- Polar Webhooks: https://polar.sh/docs/api-reference/webhooks diff --git a/_template_options/payments/_polar/billing-adapter.ts.example b/_template_options/payments/_polar/billing-adapter.ts.example index a622ab8..1682f55 100644 --- a/_template_options/payments/_polar/billing-adapter.ts.example +++ b/_template_options/payments/_polar/billing-adapter.ts.example @@ -67,3 +67,25 @@ export async function createCheckoutSession( checkoutUrl: checkout.url }; } + +export async function createCustomerPortalSession( + customerId: string, + env: PolarBillingEnv +): Promise { + const response = await fetch(`${getPolarBaseUrl(env.POLAR_SERVER)}/customer-portal/customers/${customerId}/session`, { + method: 'POST', + headers: { + Authorization: `Bearer ${env.POLAR_ACCESS_TOKEN}`, + 'Content-Type': 'application/json', + Accept: 'application/json' + } + }); + + const data = (await response.json()) as { url?: string; detail?: string }; + if (!response.ok || !data.url) { + throw new Error(data.detail ?? 'Failed to create Polar customer portal session'); + } + + return data.url; +} +