# Convex Polar Component [![npm version](https://badge.fury.io/js/@convex-dev%2Fpolar.svg)](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); }, }); ```