# Convex Polar Component [](https://badge.fury.io/js/@convex-dev%2Fpolar)
Add subscriptions and billing to your Convex app with [Polar](https://polar.sh).
**Check out the [example app](example) for a complete example.**
```tsx
// Get subscription details for the current user
// Note: getCurrentSubscription is for apps that only allow one active
// subscription per user. If you need to support multiple active
// subscriptions, use listUserSubscriptions instead.
const {
productKey,
status,
currentPeriodEnd,
currentPeriodStart,
...
} = await polar.getCurrentSubscription(ctx, {
userId: user._id,
});
// Show available plans
Upgrade to Premium
// Manage existing subscriptions
Manage Subscription
```
## Prerequisites
### Convex App
You'll need a Convex App to use the component. Follow any of the [Convex quickstarts](https://docs.convex.dev/home) to set one up.
### Polar Account
- [Create a Polar account](https://polar.sh)
- Create an organization and generate an organization token with permissions:
- `products:read`
- `products:write`
- `subscriptions:read`
- `subscriptions:write`
- `customers:read`
- `customers:write`
- `checkouts:read`
- `checkouts:write`
- `checkout_links:read`
- `checkout_links:write`
- `customer_portal:read`
- `customer_portal:write`
- `customer_sessions:write`
## Installation
Install the component package:
```ts
npm install @convex-dev/polar
```
Create a `convex.config.ts` file in your app's `convex/` folder and install the
component by calling `app.use`:
```ts
// convex/convex.config.ts
import { defineApp } from "convex/server";
import polar from "@convex-dev/polar/convex.config";
const app = defineApp();
app.use(polar);
export default app;
```
Set your Polar organization token:
```sh
npx convex env set POLAR_ORGANIZATION_TOKEN xxxxx
```
## Usage
### Set up Polar webhooks
The Polar component uses webhooks to keep subscription data in sync. You'll need to:
1. Create a webhook and webhook secret in the Polar dashboard, using your
[Convex site
URL](https://docs.convex.dev/production/environment-variables#system-environment-variables) + `/polar/events` as the webhook endpoint. It should look like this:
`https://verb-noun-123.convex.site/polar/events`
Enable the following events:
- `product.created`
- `product.updated`
- `subscription.created`
- `subscription.updated`
2. Set the webhook secret in your Convex environment:
```sh
npx convex env set POLAR_WEBHOOK_SECRET xxxxx
```
3. Register the webhook handler in your `convex/http.ts`:
```ts
import { httpRouter } from "convex/server";
import { polar } from "./example";
const http = httpRouter();
// Register the webhook handler at /polar/events
polar.registerRoutes(http as any);
export default http;
```
You can also provide callbacks for webhook events:
```ts
polar.registerRoutes(http, {
// Optional custom path, default is "/polar/events"
path: "/polar/events",
// Optional callbacks for webhook events
onSubscriptionUpdated: async (ctx, event) => {
// Handle subscription updates, like cancellations.
// Note that a cancelled subscription will not be deleted from the database,
// so this information remains available without a hook, eg., via
// `getCurrentSubscription()`.
if (event.data.customerCancellationReason) {
console.log("Customer cancelled:", event.data.customerCancellationReason);
}
},
onSubscriptionCreated: async (ctx, event) => {
// Handle new subscriptions
},
onProductCreated: async (ctx, event) => {
// Handle new products
},
onProductUpdated: async (ctx, event) => {
// Handle product updates
},
});
```
4. Be sure to run `npx convex dev` to start your Convex app with the Polar
component enabled, which will deploy the webhook handler to your Convex
instance.
### Create products in Polar
Create a product in the Polar dashboard for each pricing plan that you want to
offer. The product data will be synced to your Convex app automatically.
**Note:** You can have one price per plan, so a plan with monthly and yearly
pricing requires two products in Polar.
**Note:** The Convex Polar component is currently built to support recurring
subscriptions, and may not work as expected with one-time payments. Please
[open an issue](https://github.com/convex-dev/polar/issues) or [reach out on Discord](https://discord.gg/convex)
if you run into any issues.
Products created prior to using this component need to be synced with Convex using the `syncProducts` function.
### Initialize the Polar client
Create a Polar client in your Convex backend:
```ts
// convex/example.ts
import { Polar } from "@convex-dev/polar";
import { api, components } from "./_generated/api";
import { DataModel } from "./_generated/dataModel";
export const polar = new Polar(components.polar, {
// Required: provide a function the component can use to get the current user's ID and
// email - this will be used for retrieving the correct subscription data for the
// current user. The function should return an object with `userId` and `email`
// properties.
getUserInfo: async (ctx) => {
const user = await ctx.runQuery(api.example.getCurrentUser);
return {
userId: user._id,
email: user.email,
};
},
// Optional: Configure static keys for referencing your products.
// Alternatively you can use the `listAllProducts` function to get
// the product data and sort it out in your UI however you like
// (eg., by price, name, recurrence, etc.).
// Map your product keys to Polar product IDs (you can also use env vars for this)
// Replace these keys with whatever is useful for your app (eg., "pro", "proMonthly",
// whatever you want), and replace the values with the actual product IDs from your
// Polar dashboard
products: {
premiumMonthly: "product_id_from_polar",
premiumYearly: "product_id_from_polar",
premiumPlusMonthly: "product_id_from_polar",
premiumPlusYearly: "product_id_from_polar",
},
// Optional: Set Polar configuration directly in code
organizationToken: "your_organization_token", // Defaults to POLAR_ORGANIZATION_TOKEN env var
webhookSecret: "your_webhook_secret", // Defaults to POLAR_WEBHOOK_SECRET env var
server: "sandbox", // Optional: "sandbox" or "production", defaults to POLAR_SERVER env var
});
// Export API functions from the Polar client
export const {
changeCurrentSubscription,
cancelCurrentSubscription,
getConfiguredProducts,
listAllProducts,
generateCheckoutLink,
generateCustomerPortalUrl,
} = polar.api();
```
### Display products and prices
Use the exported `getConfiguredProducts` or `listAllProducts`function to display your products and their prices:
#### `getConfiguredProducts`
```tsx
// Simple example of displaying products and prices if you've configured
// products by key in the Polar constructor
function PricingTable() {
const products = useQuery(api.example.getConfiguredProducts);
if (!products) return null;
return (
{products.premiumMonthly && (
{products.premiumMonthly.name}
${(products.premiumMonthly.prices[0].priceAmount ?? 0) / 100}/month
)}
{products.premiumYearly && (
{products.premiumYearly.name}
${(products.premiumYearly.prices[0].priceAmount ?? 0) / 100}/year
)}
);
}
```
#### `listAllProducts`
```tsx
// Simple example of displaying products and prices if you haven't configured
// products by key in the Polar constructor
function PricingTable() {
const products = useQuery(api.example.listAllProducts);
if (!products) return null;
// You can sort through products in the client as below, or you can use
// `polar.listAllProducts` in your own Convex query and return your desired
// products to display in the UI.
const proMonthly = products.find(
(p) => p.prices[0].recurringInterval === "month"
);
const proYearly = products.find(
(p) => p.prices[0].recurringInterval === "year"
);
return (
{proMonthly && (
{proMonthly.name}
${(proMonthly.prices[0].priceAmount ?? 0) / 100}/
{proMonthly.prices[0].recurringInterval}
)}
{proYearly && (
{proYearly.name}
${(proYearly.prices[0].priceAmount ?? 0) / 100}/year
)}
);
}
```
Each product includes:
- `id`: The Polar product ID
- `name`: The product name
- `prices`: Array of prices with:
- `priceAmount`: Price in cents
- `priceCurrency`: Currency code (e.g., "USD")
- `recurringInterval`: "month" or "year"
### Add subscription UI components
Use the provided React components to add subscription functionality to your app:
```tsx
import { CheckoutLink, CustomerPortalLink } from "@convex-dev/polar/react";
import { api } from "../convex/_generated/api";
// For new subscriptions
Upgrade to Premium
// For managing existing subscriptions
Manage Subscription
```
### Handle subscription changes
The Polar component provides functions to handle subscription changes for the
current user.
**Note:** It is highly recommended to prompt the user for confirmation before
changing their subscription this way!
```ts
// Change subscription
const changeSubscription = useAction(api.example.changeCurrentSubscription);
await changeSubscription({ productId: "new_product_id" });
// Cancel subscription
const cancelSubscription = useAction(api.example.cancelCurrentSubscription);
await cancelSubscription({ revokeImmediately: true });
```
### Access subscription data
Query subscription information in your app:
```ts
// convex/example.ts
// A query that returns a user with their subscription details
export const getCurrentUser = query({
handler: async (ctx) => {
const user = await ctx.db.query("users").first();
if (!user) throw new Error("No user found");
const subscription = await polar.getCurrentSubscription(ctx, {
userId: user._id,
});
return {
...user,
subscription,
isFree: !subscription,
isPremium:
subscription?.productKey === "premiumMonthly" ||
subscription?.productKey === "premiumYearly",
};
},
});
```
## API Reference
### Polar Client
The `Polar` class accepts a configuration object with:
- `getUserInfo`: Function to get the current user's ID and email
- `products`: (Optional) Map of arbitrarily named keys to Polar product IDs
- `organizationToken`: (Optional) Your Polar organization token. Falls back to `POLAR_ORGANIZATION_TOKEN` env var
- `webhookSecret`: (Optional) Your Polar webhook secret. Falls back to `POLAR_WEBHOOK_SECRET` env var
- `server`: (Optional) Polar server environment: "sandbox" or "production". Falls back to `POLAR_SERVER` env var
### React Components
#### CheckoutLink
Props:
- `polarApi`: Object containing `generateCheckoutLink` function
- `productIds`: Array of product IDs to show in the checkout
- `children`: React children (button content)
- `embed`: (Optional) Whether to embed the checkout link. Defaults to `true`.
- `className`: (Optional) CSS class name
- `subscriptionId`: (Optional) ID of a subscription to upgrade. It must be on a free pricing.
#### CustomerPortalLink
Props:
- `polarApi`: Object containing `generateCustomerPortalUrl` function
- `children`: React children (button content)
- `className`: (Optional) CSS class name
### API Functions
#### changeCurrentSubscription
Change an existing subscription to a new plan:
```ts
await changeSubscription({ productId: "new_product_id" });
```
#### cancelCurrentSubscription
Cancel an existing subscription:
```ts
await cancelSubscription({ revokeImmediately: true });
```
#### getCurrentSubscription
Get the current user's subscription details:
```ts
const subscription = await polar.getCurrentSubscription(ctx, { userId });
```
#### listUserSubscriptions
For apps that support multiple active subscriptions per user,
get all subscriptions for a user:
```ts
const subscriptions = await polar.listUserSubscriptions(ctx, { userId });
```
#### getProducts
List all available products and their prices:
```ts
const products = await polar.listProducts(ctx);
```
#### registerRoutes
Register webhook handlers for the Polar component:
```ts
polar.registerRoutes(http, {
// Optional: customize the webhook endpoint path (defaults to "/polar/events")
path: "/custom/webhook/path",
});
```
The webhook handler uses the `webhookSecret` from the Polar client configuration or the `POLAR_WEBHOOK_SECRET` environment variable.
#### syncProducts
Sync existing products from Polar (must be run inside an action):
```ts
export const syncProducts = action({
args: {},
handler: async (ctx) => {
await polar.syncProducts(ctx);
},
});
```