Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions _template_options/payments/_polar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
22 changes: 22 additions & 0 deletions _template_options/payments/_polar/billing-adapter.ts.example
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,25 @@ export async function createCheckoutSession(
checkoutUrl: checkout.url
};
}

export async function createCustomerPortalSession(
customerId: string,
env: PolarBillingEnv
): Promise<string> {
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;
}