YeetCode
Guides

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 Resend

Webhook Events

Your backend handles five Polar webhook events:

EventWhenWhat Your Backend Does
checkout.updatedCheckout completes (success or failure)Logs the event, resolves user
order.createdPayment confirmed, order createdCreates orders record, triggers discount workflow
subscription.createdNew subscription startsCreates subscriptions record, sends confirmation email
subscription.updatedStatus changes (cancel, past_due)Updates subscription status, sends cancellation email
subscription.revokedSubscription forcibly terminatedSets 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 tiers

Field Reference

FieldRequiredDescription
idYesUnique identifier for the tier
nameYesDisplay name
iconYesLucide icon name (see available names in packages/config/src/schema.ts)
descriptionYesOne-line description shown on the pricing card
highlightedYesWhether this card gets visual emphasis
badgeNoBadge text shown on the card (e.g., "Most Popular")
priceYesNumeric price
currencyYesCurrency symbol (e.g., "$")
priceLabelNoText after the price (e.g., "/month", "/year")
priceDetailsNoAdditional context lines below the price
featuresYesArray of feature strings shown as bullet points
ctaYesCall-to-action button text
polarProductIdYesPolar product ID — must match a real product in your Polar dashboard
rankYesTier 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:

  1. Your frontend calls the createCheckoutSession action with the polarProductId
  2. The action creates a checkout session via the Polar SDK
  3. The customer is redirected to Polar's hosted checkout page
  4. 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 metadata

Status 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

  1. Customer purchases the trigger product (configured by POLAR_DISCOUNT_TRIGGER_PRODUCT_ID)
  2. The order.created webhook fires
  3. A database trigger starts the discount workflow
  4. 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 discountCodes table
    • 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 workflow
  • POLAR_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):

EventEmail TemplateSent When
Subscription createdsubscription-confirmationNew subscription starts
Subscription canceledsubscription-cancellationCustomer cancels
Order createdorder-receiptPayment confirmed
Discount code generateddiscount-codeTrigger product purchased

Email templates live in packages/email/emails/ and use values from config.yaml (app name, URLs, sender info).

On this page