Convex Polar Component 
Add subscriptions and billing to your Convex app with Polar.
// Add subscriptions to your app
const user = useQuery(api.example.getCurrentUser);
// Show available plans
<CheckoutLink
polarApi={api.example}
productIds={[products.premiumMonthly.id, products.premiumYearly.id]}
// Optional: turn off embedding to link to a checkout page
embed={false}
>
Upgrade to Premium
</CheckoutLink>
// Manage existing subscriptions
<CustomerPortalLink polarApi={api.example}>
Manage Subscription
</CustomerPortalLink>
Check out the example app for a complete example.
Prerequisites
Convex App
You'll need a Convex App to use the component. Follow any of the Convex quickstarts to set one up.
Polar Account
- Create a Polar account
- Create an organization and generate an organization token with permissions:
products:readproducts:writesubscriptions:readsubscriptions:writecustomers:readcustomers:writecheckouts:readcheckouts:writecheckout_links:readcheckout_links:writecustomer_portal:readcustomer_portal:writecustomer_sessions:write
Installation
Install the component package:
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:
// 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:
npx convex env set POLAR_ORGANIZATION_TOKEN xxxxx
Usage
1. Set up Polar webhooks
The Polar component uses webhooks to keep subscription data in sync. You'll need to:
- Create a webhook and webhook secret in the Polar dashboard, using your
Convex site
URL +
/polar/eventsas the webhook endpoint. Enable the following events:product.createdproduct.updatedsubscription.createdsubscription.updated
- Set the webhook secret in your Convex environment:
npx convex env set POLAR_WEBHOOK_SECRET xxxxx
- Register the webhook handler in your
convex/http.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:
polar.registerRoutes(http, {
// Optional custom path, default is "/events/polar"
path: "/events/polar",
// Optional callbacks for webhook events
onSubscriptionUpdated: async (ctx, event) => {
// Handle subscription updates, like cancellations
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
},
});
- Be sure to run
npx convex devto start your Convex app with the Polar component enabled, which will deploy the webhook handler to your Convex instance.
2. 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 or reach out on Discord if you run into any issues.
3. Initialize the Polar client
Create a Polar client in your Convex backend:
// 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, {
// 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 with whatever your products are (eg., "pro", "pro_monthly", whatever you want)
products: {
premiumMonthly: "product_id_from_polar",
premiumYearly: "product_id_from_polar",
premiumPlusMonthly: "product_id_from_polar",
premiumPlusYearly: "product_id_from_polar",
},
// Provide a function the component can use to get the current user's ID and email
getUserInfo: async (ctx) => {
const user = await ctx.runQuery(api.example.getCurrentUser);
return {
userId: user._id,
email: user.email,
};
},
// Optional: Configure Polar settings directly in code
// organizationToken: "your_organization_token", // Optional: Falls back to POLAR_ORGANIZATION_TOKEN env var
// webhookSecret: "your_webhook_secret", // Optional: Falls back to POLAR_WEBHOOK_SECRET env var
// server: "sandbox", // Optional: "sandbox" or "production", falls back to POLAR_SERVER env var
});
// Export the API functions
export const {
changeCurrentSubscription,
cancelCurrentSubscription,
getProducts,
listAllProducts,
} = polar.api();
export const {
generateCheckoutLink,
generateCustomerPortalUrl,
} = polar.checkoutApi();
export const _ = query(
4. Display products and prices
Use the exported getProducts or listAllProductsfunction to display your products and their prices:
// React component
const products = useQuery(api.example.getProducts);
// Simple example of displaying products and prices
function PricingTable() {
const products = useQuery(api.example.getProducts);
if (!products) return null;
return (
<div>
{products.premiumMonthly && (
<div>
<h3>{products.premiumMonthly.name}</h3>
<p>
${(products.premiumMonthly.prices[0].priceAmount ?? 0) / 100}/month
</p>
</div>
)}
{products.premiumYearly && (
<div>
<h3>{products.premiumYearly.name}</h3>
<p>
${(products.premiumYearly.prices[0].priceAmount ?? 0) / 100}/year
</p>
</div>
)}
</div>
);
}
Each product includes:
id: The Polar product IDname: The product nameprices: Array of prices with:priceAmount: Price in centspriceCurrency: Currency code (e.g., "USD")recurringInterval: "month" or "year"
4. Add subscription UI components
Use the provided React components to add subscription functionality to your app:
import { CheckoutLink, CustomerPortalLink } from "@convex-dev/polar/react";
import { api } from "../convex/_generated/api";
// For new subscriptions
<CheckoutLink
polarApi={{
generateCheckoutLink: api.example.generateCheckoutLink,
}}
productIds={[products.premiumMonthly.id, products.premiumYearly.id]}
// Optional: turn off embedding to link to a checkout page
embed={false}
>
Upgrade to Premium
</CheckoutLink>
// For managing existing subscriptions
<CustomerPortalLink
polarApi={{
generateCustomerPortalUrl: api.example.generateCustomerPortalUrl,
}}
>
Manage Subscription
</CustomerPortalLink>
5. 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!
// 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 });
6. Access subscription data
Query subscription information in your app:
// 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",
};
},
});
Example App Features
The example app demonstrates:
- Free and paid subscription tiers
- Monthly and yearly billing options
- Upgrade/downgrade between plans
- Subscription management portal
- Usage limits based on subscription tier
- Prorated billing for plan changes
API Reference
Polar Client
The Polar class accepts a configuration object with:
products: Map of product keys to Polar product IDsgetUserInfo: Function to get the current user's ID and emailorganizationToken: (Optional) Your Polar organization token. Falls back toPOLAR_ORGANIZATION_TOKENenv varwebhookSecret: (Optional) Your Polar webhook secret. Falls back toPOLAR_WEBHOOK_SECRETenv varserver: (Optional) Polar server environment: "sandbox" or "production". Falls back toPOLAR_SERVERenv var
React Components
CheckoutLink
Props:
polarApi: Object containinggenerateCheckoutLinkfunctionproductIds: Array of product IDs to show in the checkoutembed: (Optional) Whether to embed the checkout link. Defaults totrue.className: Optional CSS class namechildren: React children (button content)
CustomerPortalLink
Props:
polarApi: Object containinggenerateCustomerPortalUrlfunctionclassName: Optional CSS class namechildren: React children (button content)
API Functions
changeCurrentSubscription
Change an existing subscription to a new plan:
await changeSubscription({ productId: "new_product_id" });
cancelCurrentSubscription
Cancel an existing subscription:
await cancelSubscription({ revokeImmediately: true });
getCurrentSubscription
Get the current user's subscription details:
const subscription = await polar.getCurrentSubscription(ctx, { userId });
getProducts
List all available products and their prices:
const products = await polar.listProducts(ctx);
registerRoutes
Register webhook handlers for the Polar component:
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.