Skip to main content
The Subscriptions API manages user subscriptions through Polar (payment provider). It handles subscription lifecycle events including creation, updates, cancellation, and reactivation.

Data Model

_id
Id<'subscriptions'>
required
Unique identifier for the subscription
userId
string
required
Clerk user ID who owns the subscription
polarSubscriptionId
string
required
Polar’s subscription identifier
customerId
string
required
Polar customer ID
productId
string
required
Polar product ID
priceId
string
required
Polar price ID
status
SubscriptionStatus
required
Subscription status: active, past_due, canceled, unpaid, or trialing
interval
SubscriptionInterval
required
Billing interval: monthly or yearly
currentPeriodStart
number
required
Timestamp when the current billing period started (milliseconds)
currentPeriodEnd
number
required
Timestamp when the current billing period ends (milliseconds)
cancelAtPeriodEnd
boolean
required
Whether the subscription will cancel at the end of the current period
canceledAt
number
Timestamp when the subscription was canceled (optional)
trialStart
number
Timestamp when the trial period started (optional)
trialEnd
number
Timestamp when the trial period ends (optional)
metadata
object
Additional metadata from Polar (optional)
createdAt
number
required
Timestamp when the subscription was created
updatedAt
number
required
Timestamp when the subscription was last updated

Queries

getSubscription

Get the active subscription for the authenticated user.
import { api } from '@/convex/_generated/api';
import { useQuery } from 'convex/react';

const subscription = useQuery(api.subscriptions.getSubscription);

if (subscription) {
  console.log(`Plan: ${subscription.interval}`);
  console.log(`Status: ${subscription.status}`);
}
Arguments: None Returns: Active subscription object or null if no active subscription Authentication: Required Throws: "Unauthorized" Note: Only returns subscriptions with status === "active". Use getUserSubscriptions to get all subscriptions.

getSubscriptionByPolarId

Get a subscription by Polar subscription ID.
import { api } from '@/convex/_generated/api';
import { useQuery } from 'convex/react';

const subscription = useQuery(api.subscriptions.getSubscriptionByPolarId, {
  polarSubscriptionId: "sub_...",
});
polarSubscriptionId
string
required
The Polar subscription ID
Returns: Subscription object or null if not found Authentication: Not required (used internally by webhooks)

getUserSubscriptions

Get all subscriptions for a specific user.
import { api } from '@/convex/_generated/api';
import { useQuery } from 'convex/react';

const subscriptions = useQuery(api.subscriptions.getUserSubscriptions, {
  userId: "user_...",
});
userId
string
required
The Clerk user ID
Returns: Array of all subscriptions for the user (any status) Authentication: Not required (used internally)

Mutations

createOrUpdateSubscription

Create a new subscription or update an existing one (idempotent).
import { api } from '@/convex/_generated/api';
import { useMutation } from 'convex/react';

const upsertSubscription = useMutation(
  api.subscriptions.createOrUpdateSubscription
);

const subscriptionId = await upsertSubscription({
  polarSubscriptionId: "sub_...",
  customerId: "cus_...",
  productId: "prod_...",
  priceId: "price_...",
  status: "active",
  interval: "monthly",
  currentPeriodStart: Date.now(),
  currentPeriodEnd: Date.now() + 30 * 24 * 60 * 60 * 1000,
  cancelAtPeriodEnd: false,
});
polarSubscriptionId
string
required
Polar subscription ID
customerId
string
required
Polar customer ID
productId
string
required
Polar product ID
priceId
string
required
Polar price ID
status
SubscriptionStatus
required
Subscription status: active, past_due, canceled, unpaid, or trialing
interval
SubscriptionInterval
required
Billing interval: monthly or yearly
currentPeriodStart
number
required
Current period start timestamp (milliseconds)
currentPeriodEnd
number
required
Current period end timestamp (milliseconds)
cancelAtPeriodEnd
boolean
required
Whether to cancel at period end
canceledAt
number
Cancellation timestamp (optional)
trialStart
number
Trial start timestamp (optional)
trialEnd
number
Trial end timestamp (optional)
metadata
object
Additional metadata (optional)
userId
string
Clerk user ID (optional, will be looked up from customer if not provided)
Returns: Id<"subscriptions"> - The subscription ID (creates new or updates existing) Note: If a subscription with the same polarSubscriptionId exists, it will be updated. Otherwise, a new subscription is created. Error Handling: Returns null if customer not found and userId not provided.

markSubscriptionForCancellation

Mark a subscription to cancel at the end of the current billing period.
import { api } from '@/convex/_generated/api';
import { useMutation } from 'convex/react';

const markForCancellation = useMutation(
  api.subscriptions.markSubscriptionForCancellation
);

const subscriptionId = await markForCancellation({
  polarSubscriptionId: "sub_...",
});
polarSubscriptionId
string
required
The Polar subscription ID
Returns: Id<"subscriptions"> - The updated subscription ID Throws: "Subscription not found" Note: Sets cancelAtPeriodEnd to true. The subscription remains active until the end of the current period.

reactivateSubscription

Reactivate a subscription that was marked for cancellation.
import { api } from '@/convex/_generated/api';
import { useMutation } from 'convex/react';

const reactivate = useMutation(
  api.subscriptions.reactivateSubscription
);

const subscriptionId = await reactivate({
  polarSubscriptionId: "sub_...",
});
polarSubscriptionId
string
required
The Polar subscription ID
Returns: Id<"subscriptions"> - The updated subscription ID Throws: "Subscription not found" Note: Sets cancelAtPeriodEnd to false, allowing the subscription to continue after the current period.

revokeSubscription

Immediately cancel a subscription (revoke access).
import { api } from '@/convex/_generated/api';
import { useMutation } from 'convex/react';

const revoke = useMutation(
  api.subscriptions.revokeSubscription
);

const subscriptionId = await revoke({
  polarSubscriptionId: "sub_...",
});
polarSubscriptionId
string
required
The Polar subscription ID
Returns: Id<"subscriptions"> - The updated subscription ID Throws: "Subscription not found" Note: Sets status to "canceled" and cancelAtPeriodEnd to false. This immediately revokes access.

Database Schema

The subscriptions table has the following indexes:
// convex/schema.ts
subscriptions: defineTable({
  userId: v.string(),
  polarSubscriptionId: v.string(),
  customerId: v.string(),
  productId: v.string(),
  priceId: v.string(),
  status: subscriptionStatusEnum,
  interval: subscriptionIntervalEnum,
  currentPeriodStart: v.number(),
  currentPeriodEnd: v.number(),
  cancelAtPeriodEnd: v.boolean(),
  canceledAt: v.optional(v.number()),
  trialStart: v.optional(v.number()),
  trialEnd: v.optional(v.number()),
  metadata: v.optional(v.any()),
  createdAt: v.number(),
  updatedAt: v.number(),
})
  .index("by_userId", ["userId"])
  .index("by_polarSubscriptionId", ["polarSubscriptionId"])
  .index("by_customerId", ["customerId"])
  .index("by_status", ["status"])

Subscription Status Flow

Status Definitions:
  • trialing: Subscription is in trial period
  • active: Subscription is active and paid
  • past_due: Payment failed but subscription still active (grace period)
  • unpaid: Payment failed, subscription suspended
  • canceled: Subscription has been canceled

Example Usage

Display subscription status

import { api } from '@/convex/_generated/api';
import { useQuery } from 'convex/react';

function SubscriptionStatus() {
  const subscription = useQuery(api.subscriptions.getSubscription);

  if (!subscription) {
    return <div>No active subscription</div>;
  }

  const periodEnd = new Date(subscription.currentPeriodEnd);

  return (
    <div>
      <h3>Your Subscription</h3>
      <p>Plan: {subscription.interval}</p>
      <p>Status: {subscription.status}</p>
      <p>Renews: {periodEnd.toLocaleDateString()}</p>
      {subscription.cancelAtPeriodEnd && (
        <p>Will cancel on {periodEnd.toLocaleDateString()}</p>
      )}
    </div>
  );
}

Cancel subscription

import { api } from '@/convex/_generated/api';
import { useQuery, useMutation } from 'convex/react';

function CancelSubscriptionButton() {
  const subscription = useQuery(api.subscriptions.getSubscription);
  const markForCancellation = useMutation(
    api.subscriptions.markSubscriptionForCancellation
  );

  if (!subscription) return null;

  const handleCancel = async () => {
    if (confirm('Cancel your subscription?')) {
      await markForCancellation({
        polarSubscriptionId: subscription.polarSubscriptionId,
      });
    }
  };

  if (subscription.cancelAtPeriodEnd) {
    return <div>Subscription will cancel at period end</div>;
  }

  return (
    <button onClick={handleCancel}>
      Cancel Subscription
    </button>
  );
}

Reactivate subscription

import { api } from '@/convex/_generated/api';
import { useQuery, useMutation } from 'convex/react';

function ReactivateButton() {
  const subscription = useQuery(api.subscriptions.getSubscription);
  const reactivate = useMutation(
    api.subscriptions.reactivateSubscription
  );

  if (!subscription || !subscription.cancelAtPeriodEnd) {
    return null;
  }

  const handleReactivate = async () => {
    await reactivate({
      polarSubscriptionId: subscription.polarSubscriptionId,
    });
  };

  return (
    <button onClick={handleReactivate}>
      Reactivate Subscription
    </button>
  );
}

Webhook Integration

Subscriptions are primarily managed through Polar webhooks. The webhook handler at /api/webhooks/polar/route.ts processes events and calls these mutations: Webhook Event Handlers:
  • subscription.createdcreateOrUpdateSubscription
  • subscription.updatedcreateOrUpdateSubscription
  • subscription.canceledmarkSubscriptionForCancellation
  • subscription.revokedrevokeSubscription
  • subscription.activereactivateSubscription
Example Webhook Flow:
// Simplified from src/app/api/webhooks/polar/route.ts
switch (eventType) {
  case "subscription.created":
  case "subscription.updated":
    await convex.mutation(api.subscriptions.createOrUpdateSubscription, {
      polarSubscriptionId: subscription.id,
      customerId: subscription.customerId,
      productId: subscription.productId,
      priceId: subscription.priceId,
      status: subscription.status,
      interval: subscription.recurringInterval,
      currentPeriodStart: new Date(subscription.currentPeriodStart).getTime(),
      currentPeriodEnd: new Date(subscription.currentPeriodEnd).getTime(),
      cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
      // ...
    });
    break;

  case "subscription.canceled":
    await convex.mutation(
      api.subscriptions.markSubscriptionForCancellation,
      { polarSubscriptionId: subscription.id }
    );
    break;
}

Plan Access Helpers

The subscription system integrates with usage tracking through helper functions in convex/helpers.ts:
// Check if user has Pro access
const isPro = await hasProAccess(ctx);

// Check if user has Unlimited access
const isUnlimited = await hasUnlimitedAccess(ctx);
These helpers check for active subscriptions and determine credit limits in the Usage API.

Build docs developers (and LLMs) love