Polar Pricing & Subscriptions
Configure subscription tiers, manage the checkout flow, and understand the discount code workflow.
This guide covers how the Polar.sh subscription system works in depth. For initial setup, see Connect Polar to Convex.
How Subscriptions Work
Polar manages the billing lifecycle. Your Convex backend receives webhook events and tracks subscription state — it never handles card details or billing directly.
Customer clicks plan → Convex creates checkout session → Polar-hosted checkout
↓
Customer pays
↓
Polar sends webhook → Convex verifies + persists → DB trigger fires
↓
Email sent via ResendWebhook Events
Your backend handles five Polar webhook events:
| Event | When | What Your Backend Does |
|---|---|---|
checkout.updated | Checkout completes (success or failure) | Logs the event, resolves user |
order.created | Payment confirmed, order created | Creates orders record, triggers discount workflow |
subscription.created | New subscription starts | Creates subscriptions record, sends confirmation email |
subscription.updated | Status changes (cancel, past_due) | Updates subscription status, sends cancellation email |
subscription.revoked | Subscription forcibly terminated | Sets status to revoked |
All webhook events are deduplicated by event ID and stored in the webhooks table for audit.
Configuring Pricing Tiers
Pricing tiers live in config.yaml under the pricing key:
pricing:
- id: "free"
name: "Free"
icon: "package" # Lucide icon name (autocomplete in VS Code)
description: "Get started for free."
highlighted: false # Visual emphasis on the pricing card
badge: null # Optional badge text (e.g., "Most Popular")
price: 0
currency: "$"
priceLabel: "/month"
features: # Bullet points on the pricing card
- "Feature one"
- "Community support"
cta: "Get Started" # Button text
polarProductId: "prod_xxx" # Must match your Polar product
rank: 1 # Higher rank includes all lower tiersField Reference
| Field | Required | Description |
|---|---|---|
id | Yes | Unique identifier for the tier |
name | Yes | Display name |
icon | Yes | Lucide icon name (see available names in packages/config/src/schema.ts) |
description | Yes | One-line description shown on the pricing card |
highlighted | Yes | Whether this card gets visual emphasis |
badge | No | Badge text shown on the card (e.g., "Most Popular") |
price | Yes | Numeric price |
currency | Yes | Currency symbol (e.g., "$") |
priceLabel | No | Text after the price (e.g., "/month", "/year") |
priceDetails | No | Additional context lines below the price |
features | Yes | Array of feature strings shown as bullet points |
cta | Yes | Call-to-action button text |
polarProductId | Yes | Polar product ID — must match a real product in your Polar dashboard |
rank | Yes | Tier hierarchy — used to determine what "Everything in Free" means |
Connecting to Polar Products
Each tier's polarProductId must match a product you've created in the Polar dashboard. The ID is visible in the product settings URL or via the Polar API.
When a customer clicks the CTA button:
- Your frontend calls the
createCheckoutSessionaction with thepolarProductId - The action creates a checkout session via the Polar SDK
- The customer is redirected to Polar's hosted checkout page
- After payment, Polar sends webhooks to your Convex backend
Subscription Lifecycle
Database Schema
Subscriptions are tracked in the subscriptions table:
subscriptions
├── polarId — Polar subscription ID
├── userId — Convex user ID
├── polarCustomerId — Polar customer ID
├── polarProductId — Which product they subscribed to
├── status — "active" | "canceled" | "past_due" | "revoked"
├── currentPeriodEnd — When the current billing period ends
├── canceledAt — When the customer canceled (if applicable)
└── metadata — Raw Polar metadataStatus Transitions
active → canceled (customer cancels, access continues until period end)
active → past_due (payment failed, Polar retrying)
active → revoked (forcibly terminated)
canceled → active (customer resubscribes)
past_due → active (payment succeeds on retry)
past_due → revoked (all retries exhausted)Checking Subscription Status
In the app (authenticated dashboard), the billing page queries subscription status:
const billing = useQuery(api.api.app.billing.getUserBillingOverview);
// Returns: { subscription, purchasedProductIds, memberSince }The marketing site checks purchased product IDs to show "Current Plan" indicators on pricing cards.
Customer Portal
Polar provides a hosted customer portal where subscribers can:
- View their current subscription
- Update payment method
- Cancel or resubscribe
- View billing history
The portal is accessed via a session token:
const createPortalSession = useAction(
api.api.app.billing.createCustomerPortalSession,
);
const result = await createPortalSession({ returnUrl: window.location.href });
window.open(result.customerPortalUrl, "_blank");This is wired up in the billing page's "Manage subscription" button.
Discount Code Workflow
YeetCode includes an automated discount code workflow powered by Convex's durable workflow system. When a customer purchases a specific "trigger" product, they automatically receive a discount code for another product.
How It Works
- Customer purchases the trigger product (configured by
POLAR_DISCOUNT_TRIGGER_PRODUCT_ID) - The
order.createdwebhook fires - A database trigger starts the discount workflow
- The workflow:
- Creates a 100% off discount on Polar (single-use, 30-day expiry)
- Generates a code in the format
YEET+ 8 random characters - Saves the code to the
discountCodestable - Sends the code to the customer via email
Configuration
Set these environment variables in your Convex dashboard:
POLAR_DISCOUNT_TRIGGER_PRODUCT_ID— The product that triggers the workflowPOLAR_DISCOUNT_PRODUCT_ID— The product the discount code applies to
Disabling Discounts
If you don't need the discount workflow, set both env vars to placeholder values. The workflow will attempt to run but won't match any product IDs.
Emails
Polar subscription events trigger these emails (via Resend):
| Event | Email Template | Sent When |
|---|---|---|
| Subscription created | subscription-confirmation | New subscription starts |
| Subscription canceled | subscription-cancellation | Customer cancels |
| Order created | order-receipt | Payment confirmed |
| Discount code generated | discount-code | Trigger product purchased |
Email templates live in packages/email/emails/ and use values from config.yaml (app name, URLs, sender info).