npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mmailaender/convex-creem

v0.3.0

Published

A Creem component for Convex.

Readme

Convex Creem Component

Add subscriptions, one-time purchases, and billing to your Convex app with Creem.

Check out the Svelte example and React example for complete integrations.

Table of Contents


Quick Start — Backend

Complete these steps to use the billing API from your Convex functions. No frontend framework required.

1. Install

npm install @mmailaender/convex-creem

2. Register component

// convex/convex.config.ts
import { defineApp } from "convex/server";
import creem from "@mmailaender/convex-creem/convex.config";

const app = defineApp();
app.use(creem);

export default app;

3. Set environment variables

npx convex env set CREEM_API_KEY <your_creem_api_key>
npx convex env set CREEM_WEBHOOK_SECRET <your_creem_webhook_signing_secret>

4. Configure billing

// convex/billing.ts
import { Creem, type ApiResolver } from "@mmailaender/convex-creem";
import { api, components } from "./_generated/api";
import { query, internalAction } from "./_generated/server";

export const creem = new Creem(components.creem);

// Auth resolver — replace with your own auth logic
const resolve: ApiResolver = async (ctx) => {
  const user = await ctx.runQuery(api.users.currentUser);
  return {
    userId: user._id,
    email: user.email,
    entityId: user._id, // For org billing: user.activeOrgId ?? user._id
  };
};

// Generate Convex function exports — each calls resolve(), then delegates
const {
  uiModel,
  snapshot,
  checkouts,
  subscriptions,
  products,
  customers,
  orders,
} = creem.api({ resolve });

export { uiModel, snapshot };
export const checkoutsCreate = checkouts.create;
export const subscriptionsUpdate = subscriptions.update;
export const subscriptionsCancel = subscriptions.cancel;
export const subscriptionsResume = subscriptions.resume;
export const subscriptionsPause = subscriptions.pause;
export const subscriptionsList = subscriptions.list;
export const subscriptionsListAll = subscriptions.listAll;
export const productsList = products.list;
export const productsGet = products.get;
export const customersRetrieve = customers.retrieve;
export const customersPortalUrl = customers.portalUrl;
export const ordersList = orders.list;

// Sync products from Creem (CLI / dashboard only)
export const syncBillingProducts = internalAction({
  args: {},
  handler: async (ctx) => {
    await creem.syncProducts(ctx);
  },
});

5. Register webhooks

// convex/http.ts
import { httpRouter } from "convex/server";
import { creem } from "./billing";

const http = httpRouter();

creem.registerRoutes(http);

export default http;

Use your Convex site URL + /creem/events as the webhook endpoint in your Creem dashboard. The component automatically handles checkout.completed, subscription.*, and product.* events.

For custom event handlers (e.g. sending emails on checkout), see Webhook event middleware.

6. Sync products

After configuring webhooks, pull your Creem products into the Convex database:

npx convex run billing:syncBillingProducts

This is an internalAction — it can only be triggered from the CLI or the Convex dashboard.

You're done with the backend. You can now call api.billing.* from your frontend or other Convex functions. If you only need the API (no UI widgets), skip ahead to the API Reference.


Quick Start — Frontend (UI Widgets)

The component ships pre-built Svelte and React widgets that handle checkout, plan switching, cancellation, seat management, and billing state — all connected to Convex. Complete these three extra steps to use them.

7. Install UI primitives

The widgets are built on Ark UI headless primitives. Install the adapter for your framework:

React

npm install @ark-ui/react

Svelte

npm install @ark-ui/svelte

8. Install Tailwind CSS

The widgets use Tailwind CSS v4. If your project doesn't have Tailwind yet, install it following the official guide.

9. Import styles

Add the component's design system import to your CSS entry point, after the Tailwind import:

@import "tailwindcss";
@import "@mmailaender/convex-creem/styles";

This registers the component's design tokens, base styles, and @source directives so Tailwind scans the library's component files automatically.

You're ready to use the UI widgets. Continue with the scenarios below.


Entity Model

By default, billing is scoped to the authenticated user — the entityId returned from your resolver is used as the billing entity. All creem.api({ resolve }) functions, checkout metadata, and webhook resolution automatically use this entity.

For organization or team billing, return the org ID as entityId:

const resolve: ApiResolver = async (ctx) => {
  const user = await ctx.runQuery(api.users.currentUser);
  const org = await ctx.runQuery(api.orgs.getActiveOrg);
  return {
    userId: user._id,
    email: user.email,
    entityId: org?._id ?? user._id, // org billing or personal billing
  };
};

All billing operations scope to the entityId. Webhooks resolve convexBillingEntityId from checkout metadata (falls back to convexUserId). No other code changes needed.

For access control details, see Security & Access Control.


Scenarios

Both Svelte and React widgets share identical props and APIs — only the import path and framework boilerplate differ.

Convention: Where the markup is identical in both frameworks, examples are shown once. The only recurring difference is class= (Svelte) vs className= (React) when passing CSS classes. Where Svelte and React syntax diverges (e.g. children rendering), both versions are shown.

Wire the billing API

Every widget needs a ConnectedBillingApi object. Create it once in your layout or page component:

Svelte

<script lang="ts">
  import { setupConvex } from "@mmailaender/convex-svelte";
  import {
    Subscription, Product, BillingPortal,
    type ConnectedBillingApi,
  } from "@mmailaender/convex-creem/svelte";
  import { api } from "../convex/_generated/api.js";

  setupConvex(import.meta.env.VITE_CONVEX_URL);

  const billingApi: ConnectedBillingApi = {
    uiModel: api.billing.uiModel,
    checkouts: { create: api.billing.checkoutsCreate },
    subscriptions: {
      update: api.billing.subscriptionsUpdate,
      cancel: api.billing.subscriptionsCancel,
      resume: api.billing.subscriptionsResume,
    },
    customers: { portalUrl: api.billing.customersPortalUrl },
  };
</script>

React

import {
  Subscription,
  Product,
  BillingPortal,
  type ConnectedBillingApi,
} from "@mmailaender/convex-creem/react";
import { api } from "../convex/_generated/api";

const billingApi: ConnectedBillingApi = {
  uiModel: api.billing.uiModel,
  checkouts: { create: api.billing.checkoutsCreate },
  subscriptions: {
    update: api.billing.subscriptionsUpdate,
    cancel: api.billing.subscriptionsCancel,
    resume: api.billing.subscriptionsResume,
  },
  customers: { portalUrl: api.billing.customersPortalUrl },
};

The ConnectedBillingApi object is the same shape in both frameworks. Only the Convex client setup differs: setupConvex() in Svelte vs <ConvexProvider> in React (see @mmailaender/convex-svelte and convex/react docs).

1. Subscriptions

1.1 Standard subscription plans

A typical pricing page with Free / Basic / Premium / Enterprise tiers. The billing toggle auto-derives from the cycles present in registered plans.

<Subscription.Root api={billingApi}>
  <Subscription.Item type="free" title="Free" description="Up to 3 users" />
  <Subscription.Item
    planId="basic"
    type="single"
    productIds={{
      "every-month": "prod_basic_monthly",
      "every-year": "prod_basic_yearly",
    }}
  />
  <Subscription.Item
    planId="premium"
    type="single"
    recommended
    productIds={{
      "every-month": "prod_premium_monthly",
      "every-year": "prod_premium_yearly",
    }}
  />
  <Subscription.Item
    type="enterprise"
    title="Enterprise"
    contactUrl="https://example.com/sales"
  />
</Subscription.Root>
<BillingPortal api={billingApi} />

What you get:

  • Pricing cards with auto-resolved titles, descriptions (rendered as Markdown), and prices from Creem product data
  • Billing cycle toggle (monthly/yearly) — hidden when all plans share a single cycle
  • "Current plan" badge on the active subscription
  • Plan switching with confirmation dialog
  • Trial countdown badge
  • Cancel / resume subscription (with confirmation dialog)
  • Scheduled cancellation banner with "Undo" button

1.2 Seat-based subscriptions

Two workflows for seat-based pricing:

User-selectable seats — the customer picks a quantity before checkout:

<Subscription.Root api={billingApi} showSeatPicker>
  <Subscription.Item
    type="seat-based"
    productIds={{ "every-month": "prod_team_monthly" }}
  />
  <Subscription.Item
    type="seat-based"
    productIds={{ "every-month": "prod_business_monthly" }}
  />
</Subscription.Root>

Auto-derived seats — pass a fixed count (e.g. org member count) to checkout:

<Subscription.Root api={billingApi} units={orgMemberCount}>
  <Subscription.Item
    type="seat-based"
    productIds={{ "every-month": "prod_team_monthly" }}
  />
</Subscription.Root>

When subscriptions.update is provided in the API, active seat-based plans show a "Change seats" control.

Tip: For auto-derived seats, keep the subscription in sync with your data. When your member count changes, call subscriptions.update with the new units so the billing reflects the current seat count.

2. Products

2.1 Single one-time product

A standalone product purchased once. Shows "Owned" after purchase:

<Product.Root api={billingApi}>
  <Product.Item type="one-time" productId="prod_license" />
</Product.Root>

2.2 Repeating product (consumable)

Can be purchased multiple times — no "Owned" badge:

<Product.Root api={billingApi}>
  <Product.Item type="recurring" productId="prod_credits" title="100 Credits" />
</Product.Root>

2.3 Mutually exclusive product group

Use the transition prop to define upgrade paths between products. When the user owns a lower-tier product, only valid upgrade paths are shown:

<Product.Root
  api={billingApi}
  transition={[
    {
      from: "prod_basic_license",
      to: "prod_premium_license",
      kind: "via_product",
      viaProductId: "prod_basic_to_premium_upgrade",
    },
  ]}
>
  <Product.Item type="one-time" productId="prod_basic_license" />
  <Product.Item type="one-time" productId="prod_premium_license" />
</Product.Root>

Transition kinds:

  • via_product — checkout uses a dedicated upgrade product (delta pricing)
  • direct — checkout uses the target product directly

3. Billing Portal

<BillingPortal> opens the Creem customer billing portal. It auto-hides when the billing entity has no Creem customer record.

Pass permissions to control who can access the portal (e.g. only admins):

<BillingPortal api={billingApi} permissions={{ canAccessPortal: isAdmin }} />
<!-- After a subscription group -->
<BillingPortal api={billingApi} />

<!-- Standalone with custom label -->
<BillingPortal api={billingApi}>Manage billing & invoices</BillingPortal>

4. Feature Gating

Use BillingGate to conditionally render UI based on available billing actions:

Svelte

<BillingGate snapshot={billingSnapshot} requiredActions="portal">
  {#snippet children()}
    <p>You have portal access.</p>
  {/snippet}
  {#snippet fallback()}
    <p>Upgrade to access the billing portal.</p>
  {/snippet}
</BillingGate>

React

<BillingGate
  snapshot={billingSnapshot}
  requiredActions="portal"
  fallback={<p>Upgrade to access the billing portal.</p>}
>
  <p>You have portal access.</p>
</BillingGate>

Available actions: checkout, portal, cancel, reactivate, switch_interval, update_seats, contact_sales.

5. Checkout Success

Show a confirmation banner when the user returns from checkout. The component parses Creem's query parameters automatically:

<CheckoutSuccessSummary />

Advanced

Webhook event middleware

registerRoutes accepts an optional events object to run app-specific logic alongside the component's automatic handling:

creem.registerRoutes(http, {
  path: "/creem/events", // default
  events: {
    "checkout.completed": async (ctx, event) => {
      // ctx is a Convex mutation context
      // event has { type, data } from Creem
      // Example: send confirmation email, grant entitlements, log analytics
    },
    "subscription.updated": async (ctx, event) => {
      const data = event.data as { customerCancellationReason?: string };
      if (data?.customerCancellationReason) {
        console.log("Cancellation reason:", data.customerCancellationReason);
      }
    },
  },
});

Your handlers run after the component's built-in processing (customer/subscription/order upserts). The ctx is a Convex mutation context — you can read/write to your own tables.

Supported events: checkout.completed, subscription.active, subscription.updated, subscription.canceled, subscription.paused, subscription.resumed, product.created, product.updated.

Security & Access Control

Auth is the app's responsibility. The component is a sync engine — it reads from Convex DB and writes to the Creem API. Every class method takes explicit args; there is no hidden auth layer.

Choose your approach:

  • Quick start — use creem.api({ resolve }) to generate ready-to-export Convex functions. Each one calls your resolve callback to authenticate and determine the entityId. The billing entity is derived from the authenticated session, never from client input. See Step 4: Configure billing and the creem.api({ resolve }) reference.

  • Full control — use the resource namespaces (creem.subscriptions.*, creem.checkouts.*, etc.) directly in your own Convex functions. You handle auth, entity resolution, and permission checks yourself.

    The library exports shared arg validators that match exactly what the connected widgets send. Use them to keep your custom functions in sync:

    | Export | Used by | | ------------------------ | ------------------------------------------------ | | checkoutCreateArgs | <Subscription.Root>, <Product.Root> | | subscriptionUpdateArgs | <Subscription.Root> (plan switch, seat update) | | subscriptionCancelArgs | <Subscription.Root> (cancel button) | | subscriptionResumeArgs | <Subscription.Root> (resume button) | | subscriptionPauseArgs | <Subscription.Root> (pause button) |

    Example:

// convex/billing.ts
import {
  Creem,
  checkoutCreateArgs,
  subscriptionCancelArgs,
} from "@mmailaender/convex-creem";
import { ConvexError } from "convex/values";
import { action, mutation } from "./_generated/server";
import { api, components } from "./_generated/api";

const creem = new Creem(components.creem);

async function resolveAuth(ctx) {
  const session = await ctx.runQuery(api.auth.getSession);
  if (!session) throw new ConvexError("Not authenticated");
  const org = await ctx.runQuery(api.orgs.getActiveOrg);
  return {
    userId: session.userId,
    email: session.user.email,
    entityId: org?._id ?? session.userId,
    role: session.user.role,
  };
}

// Admin-only: create checkout
export const checkoutsCreate = action({
  args: checkoutCreateArgs,
  handler: async (ctx, args) => {
    const auth = await resolveAuth(ctx);
    if (auth.role !== "admin") throw new ConvexError("Forbidden");
    return await creem.checkouts.create(ctx, {
      entityId: auth.entityId,
      userId: auth.userId,
      email: auth.email,
      ...args,
    });
  },
});

// Admin-only: cancel subscription
export const subscriptionsCancel = mutation({
  args: subscriptionCancelArgs,
  handler: async (ctx, args) => {
    const auth = await resolveAuth(ctx);
    if (auth.role !== "admin") throw new ConvexError("Forbidden");
    await creem.subscriptions.cancel(ctx, { entityId: auth.entityId, ...args });
  },
});

UI-side permissionsBillingPermissions controls which buttons are enabled in the widgets. This is cosmetic gating only; enforce real permissions server-side.

type BillingPermissions = {
  canCheckout?: boolean;
  canChangeSubscription?: boolean;
  canCancelSubscription?: boolean;
  canResumeSubscription?: boolean;
  canUpdateSeats?: boolean;
  canAccessPortal?: boolean;
};
<script lang="ts">
  const isAdmin = $derived(currentUser?.role === "admin" || currentUser?.role === "owner");
  const permissions = $derived({
    canCheckout: isAdmin,
    canChangeSubscription: isAdmin,
    canCancelSubscription: isAdmin,
    canResumeSubscription: isAdmin,
    canUpdateSeats: isAdmin,
  });
</script>

<Subscription.Root api={billingApi} {permissions}>
  ...
</Subscription.Root>

When a permission is false, the button renders as disabled (greyed out). When omitted or undefined, all actions default to enabled.

Pre-checkout gate — onBeforeCheckout

Both <Subscription.Root> and <Product.Root> accept an onBeforeCheckout callback that fires before the widget calls checkouts.create. Return true to proceed, false to abort silently.

This is a generic hook — use it for authentication gates, terms acceptance, confirmation dialogs, analytics, or any logic that must run before checkout.

<Subscription.Root
  api={billingApi}
  onBeforeCheckout={(intent) => {
    if (!currentUser) {
      pendingCheckout.save(intent);
      openSignInDialog();
      return false;
    }
    return true;
  }}
>
  ...
</Subscription.Root>

CheckoutIntent — the object passed to the callback:

type CheckoutIntent = {
  productId: string;
  units?: number;
};

Auto-resume after sign-in

The widget automatically resumes a pending checkout when the user becomes authenticated. The full flow:

  1. Unauthenticated user clicks "Subscribe" → onBeforeCheckout fires
  2. Your callback saves the intent via pendingCheckout.save(intent), opens your sign-in dialog/redirect, and returns false
  3. After sign-in, the Convex query re-fires → model.user becomes non-null
  4. The widget detects the pending checkout and auto-triggers checkouts.create

This works for both modal auth (Clerk, Auth0 popup) and redirect auth (OAuth) — no manual resume code needed.

Safety: The widget skips auto-resume if the user already has an active subscription (<Subscription.Root>) or already owns the product (<Product.Root>), preventing duplicate purchases after sign-in.

pendingCheckout is a tiny sessionStorage-based helper exported from the library:

import { pendingCheckout } from "@mmailaender/convex-creem/svelte";

pendingCheckout.save(intent); // store before sign-in
pendingCheckout.load(); // read + auto-clear (used internally by widgets)
pendingCheckout.clear(); // manual clear if needed

Custom billing UI model

uiModel (from creem.api({ resolve })) returns everything the connected widgets need. If you need app-specific fields, write your own query using creem.getBillingModel():

import { query } from "./_generated/server";

export const getCustomBillingModel = query({
  args: {},
  handler: async (ctx) => {
    const user = await currentUser(ctx);
    const billingData = await creem.getBillingModel(ctx, {
      entityId: user?._id ?? null,
      user: user ? { _id: user._id, email: user.email } : null,
    });
    return {
      ...billingData,
      teamSize: user?.teamSize,
      featureFlags: user?.featureFlags,
    };
  },
});

Server endpoint overrides

Only needed for non-default API endpoints (e.g. test/staging):

npx convex env set CREEM_SERVER_IDX <index>
# or
npx convex env set CREEM_SERVER_URL <url>

Leave both unset to use the default Creem production endpoint.


API Reference

Resource namespaces — creem.<namespace>.*

All methods take explicit arguments. Use them directly in your own Convex functions, or let creem.api({ resolve }) generate ready-to-export wrappers.

creem.subscriptions.*

| Method | Data source | Description | | ---------------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | .getCurrent(ctx, { entityId }) | Convex DB | Current active subscription with product join | | .list(ctx, { entityId }) | Convex DB | Active subscriptions (excludes ended + expired trials) | | .listAll(ctx, { entityId }) | Convex DB | All subscriptions including ended | | .update(ctx, { entityId, subscriptionId?, productId?, units?, updateBehavior? }) | Creem API | Unified plan switch (productId) or seat update (units). Pass subscriptionId when the entity has multiple active subscriptions. Optional proration override. | | .cancel(ctx, { entityId, revokeImmediately? }) | Creem API | Cancel subscription | | .pause(ctx, { entityId }) | Creem API | Pause an active subscription | | .resume(ctx, { entityId }) | Creem API | Resume a paused or scheduled-cancel subscription |

creem.checkouts.*

| Method | Data source | Description | | ------------------------------------------------------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------- | | .create(ctx, { entityId, userId, email, productId, successUrl?, fallbackSuccessUrl?, units?, metadata?, theme? }) | Creem API | Create checkout URL with 3-tier successUrl resolution and optional theme |

creem.products.*

| Method | Data source | Description | | -------------------------- | ----------- | --------------------------------------------------- | | .list(ctx, options?) | Convex DB | All synced products (public — no entityId needed) | | .get(ctx, { productId }) | Convex DB | Single product by ID (public) |

creem.customers.*

| Method | Data source | Description | | ------------------------------- | ----------- | --------------------------- | | .retrieve(ctx, { entityId }) | Convex DB | Customer record by entity | | .portalUrl(ctx, { entityId }) | Creem API | Customer billing portal URL |

creem.orders.*

| Method | Data source | Description | | -------------------------- | ----------- | -------------------- | | .list(ctx, { entityId }) | Convex DB | Paid one-time orders |

Composite helpers (top-level methods)

| Method | Description | | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | creem.getBillingModel(ctx, { entityId, user? }) | Aggregates snapshot + products + subscriptions + orders into a single object for widgets. Graceful when entityId is null (returns public catalog only). | | creem.getBillingSnapshot(ctx, { entityId }) | Resolved billing state (plan, status, available actions). Uses resolvePlan override if configured, otherwise built-in resolver. |

creem.api({ resolve }) — convenience exports

Generates ready-to-export Convex function definitions. Each function calls your resolve callback, then delegates to the corresponding namespace method.

| Export | Wraps | Type | Description | | ----------------------- | ----------------------- | ------ | ---------------------------------------------------------------------------------- | | uiModel | getBillingModel | query | Calls resolve(), then getBillingModel. Graceful when unauthenticated. | | snapshot | getBillingSnapshot | query | Calls resolve(), then getBillingSnapshot. Returns null when unauthenticated. | | checkouts.create | checkouts.create | action | Auto-resolves auth | | subscriptions.update | subscriptions.update | action | Auto-resolves auth | | subscriptions.cancel | subscriptions.cancel | action | Auto-resolves auth | | subscriptions.resume | subscriptions.resume | action | Auto-resolves auth | | subscriptions.pause | subscriptions.pause | action | Auto-resolves auth | | subscriptions.list | subscriptions.list | query | Auto-resolves auth | | subscriptions.listAll | subscriptions.listAll | query | Auto-resolves auth | | products.list | products.list | query | Public, no auth needed | | products.get | products.get | query | Public, no auth needed | | customers.retrieve | customers.retrieve | query | Auto-resolves auth | | customers.portalUrl | customers.portalUrl | action | Auto-resolves auth | | orders.list | orders.list | query | Auto-resolves auth |

Infrastructure

| Method | Description | | ------------------------------------------------ | ------------------------------------------- | | creem.syncProducts(ctx) | Pull products from Creem API into Convex DB | | creem.registerRoutes(http, { path?, events? }) | Register webhook HTTP routes |

Direct API access — creem.sdk.*

The resource namespaces above cover all billing features that stay in sync with Convex via webhooks. Some Creem API resources have no webhook support, so the component cannot mirror them in Convex DB. For these, use creem.sdk.* directly inside your own Convex actions — it's the same Creem SDK client, already configured with your API key:

| Resource | Synced to Convex? | Access | | ---------------- | -------------------- | -------------------------- | | Subscriptions | Yes (webhook) | creem.subscriptions.* | | Checkouts | Yes (webhook) | creem.checkouts.* | | Products | Yes (webhook + sync) | creem.products.* | | Customers | Yes (webhook) | creem.customers.* | | Orders | Yes (webhook) | creem.orders.* | | Licenses | No webhook | creem.sdk.licenses.* | | Discounts | No webhook | creem.sdk.discounts.* | | Transactions | No webhook | creem.sdk.transactions.* |

import { action } from "./_generated/server";
import { v } from "convex/values";

// Example: create a discount (not synced — Creem has no webhook for discounts)
export const createDiscount = action({
  args: { code: v.string(), percentage: v.number() },
  handler: async (ctx, args) => {
    return await creem.sdk.discounts.create({
      name: args.code,
      code: args.code,
      type: "percentage",
      percentage: args.percentage,
      duration: "forever",
      appliesTo: [],
    });
  },
});

Component Reference

All components share identical props across Svelte and React.

  • Import: @mmailaender/convex-creem/svelte or @mmailaender/convex-creem/react
  • CSS class prop: class in Svelte, className in React
  • Children: Svelte Snippet / React ReactNode
  • Svelte components use Svelte 5 runes and snippet rendering ({@render ...})

See the Svelte example and React example for complete integrations.

Widgets

These query Convex directly and manage billing state end-to-end.

<Subscription.Root>

Container for subscription plan cards. Handles billing cycle toggle, checkout, plan switching, cancellation, and seat management.

| Prop | Type | Default | Description | | ------------------- | --------------------------------------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | api | ConnectedBillingApi | — | Required. Backend function references | | permissions | BillingPermissions | all enabled | Disable actions based on user role | | class/className | string | "" | Wrapper CSS class | | successUrl | string | product's defaultSuccessUrl → current page | Override redirect after checkout. When omitted, uses the product's defaultSuccessUrl from Creem; if that is also unset, falls back to the current page. | | units | number | — | Auto-derived seat count for seat-based plans | | showSeatPicker | boolean | false | Show quantity picker on seat-based cards | | twoColumnLayout | boolean | false | Use two-column card layout | | updateBehavior | UpdateBehavior | "proration-charge-immediately" | How plan switches and seat updates are billed. See below. | | onBeforeCheckout | (intent: CheckoutIntent) => Promise<boolean> \| boolean | — | Gate checkout (auth, terms, etc.). Return false to abort. | | children | Snippet / ReactNode | — | <Subscription.Item> children |

UpdateBehavior controls how the Creem API handles plan switches and seat changes:

  • "proration-charge-immediately" — prorate and charge the difference now (default)
  • "proration-charge" — prorate, charge on next invoice
  • "proration-none" — no proration, change takes effect on next billing cycle

<Subscription.Item>

Registers a plan inside <Subscription.Root>. Renders nothing on its own — the root component renders the pricing cards.

| Prop | Type | Default | Description | | ------------- | ---------------------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------- | | type | "free" \| "single" \| "seat-based" \| "enterprise" | — | Required. Plan type | | planId | string | first product ID or type | Unique plan identifier | | title | string | from Creem product data | Plan display title | | description | string | from Creem product data | Plan subtitle (rendered as Markdown) | | contactUrl | string | — | "Contact sales" link. Required when type="enterprise". | | recommended | boolean | false | Highlight as recommended plan | | productIds | Partial<Record<RecurringCycle, string>> | — | Creem product IDs keyed by billing cycle. Required when type="single" or type="seat-based". |

Supported billing cycles: every-month, every-three-months, every-six-months, every-year.

Subscription and Subscription.Item are aliases — use whichever reads better in your markup.

<Product.Root>

Container for one-time or repeating product cards. Handles ownership tracking, upgrade transitions, and checkout.

| Prop | Type | Default | Description | | ------------------- | --------------------------------------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | api | ConnectedBillingApi | — | Required. Backend function references | | permissions | BillingPermissions | all enabled | Disable actions based on user role | | transition | Transition[] | [] | Upgrade path rules between products | | class/className | string | "" | Wrapper CSS class | | layout | "default" \| "single" | "default" | Card layout mode | | styleVariant | "legacy" \| "pricing" | "legacy" | Visual style variant | | showImages | boolean | false | Show product images on cards | | pricingCtaVariant | "filled" \| "faded" | "faded" | Call-to-action button style | | successUrl | string | product's defaultSuccessUrl → current page | Override redirect after checkout. When omitted, uses the product's defaultSuccessUrl from Creem; if that is also unset, falls back to the current page. | | onBeforeCheckout | (intent: CheckoutIntent) => Promise<boolean> \| boolean | — | Gate checkout (auth, terms, etc.). Return false to abort. | | children | Snippet / ReactNode | — | <Product.Item> children |

Transition types:

type Transition =
  | { from: string; to: string; kind: "direct" }
  | { from: string; to: string; kind: "via_product"; viaProductId: string };

<Product.Item>

Registers a product inside <Product.Root>.

| Prop | Type | Default | Description | | ------------- | --------------------------- | ----------------------- | --------------------------------------------------------- | | productId | string | — | Required. Creem product ID | | type | "one-time" \| "recurring" | — | Required. One-time shows "Owned" badge after purchase | | title | string | from Creem product data | Card display title | | description | string | from Creem product data | Card subtitle (rendered as Markdown) |

Product and Product.Item are aliases.

<BillingPortal>

Button that opens the Creem customer billing portal. Auto-hides when the billing entity has no Creem customer record, or when canAccessPortal is false.

| Prop | Type | Default | Description | | ------------------- | ----------------------- | ------------------ | --------------------------------------------------------- | | api | ConnectedBillingApi | — | Required. Backend function references | | permissions | BillingPermissions | all enabled | Control portal access (e.g. { canAccessPortal: false }) | | class/className | string | "" | Button CSS class | | children | Snippet / ReactNode | "Manage billing" | Custom button label |

Presentational components

Lower-level building blocks for custom layouts. These do not call Convex directly — pass data and callbacks as props.

<PricingSection>

Renders a grid of pricing cards with an optional billing cycle toggle.

| Prop | Type | Description | | ----------------------- | ------------------------- | ------------------------------------- | | plans | UIPlanEntry[] | Plan definitions | | snapshot | BillingSnapshot \| null | Current billing state | | selectedCycle | RecurringCycle | Active billing cycle | | products | ConnectedProduct[] | Product data for price resolution | | subscriptionProductId | string \| null | Currently subscribed product | | subscriptionStatus | string \| null | Subscription status | | units | number | Seat count | | showSeatPicker | boolean | Show quantity picker | | subscribedSeats | number \| null | Current seat count | | isGroupSubscribed | boolean | Whether group has active subscription | | disableCheckout | boolean | Disable checkout buttons | | disableSwitch | boolean | Disable plan switch buttons | | disableSeats | boolean | Disable seat controls | | onCycleChange | (cycle) => void | Billing cycle change handler | | onCheckout | (payload) => void | Checkout handler | | onSwitchPlan | (payload) => void | Plan switch handler | | onUpdateSeats | (payload) => void | Seat update handler |

<PricingCard>

A single plan card with price, description, and action button. Same props as <PricingSection> for a single plan (see source for full list).

<BillingToggle>

Billing cycle segment control (e.g. Monthly / Yearly).

| Prop | Type | Description | | --------------- | ------------------ | ---------------- | | cycles | RecurringCycle[] | Available cycles | | value | RecurringCycle | Selected cycle | | onValueChange | (cycle) => void | Change handler | | className | string | CSS class |

<CheckoutButton>

Styled checkout button. Supports both onCheckout callback and href link modes.

| Prop | Type | Description | | ------------ | ----------------------- | ---------------------------------- | | productId | string | Product ID | | href | string | Link mode: direct URL | | disabled | boolean | Disable button | | className | string | CSS class | | onCheckout | (payload) => void | Callback mode: { productId } | | children | Snippet / ReactNode | Button label (default: "Checkout") |

<OneTimeCheckoutButton>

Same as <CheckoutButton> with default label "Buy now".

<CustomerPortalButton>

Styled button for opening the customer billing portal.

| Prop | Type | Description | | -------------- | ----------------------- | ---------------------------------------- | | href | string | Link mode: direct URL | | disabled | boolean | Disable button | | className | string | CSS class | | onOpenPortal | () => void | Callback mode | | children | Snippet / ReactNode | Button label (default: "Manage billing") |

<BillingGate>

Conditionally renders children based on available billing actions.

| Prop | Type | Description | | ----------------- | -------------------------------------- | --------------------------------------- | | snapshot | BillingSnapshot \| null | Current billing state | | requiredActions | AvailableAction \| AvailableAction[] | Actions that must be available | | children | Snippet / ReactNode | Rendered when all actions are available | | fallback | Snippet / ReactNode | Rendered otherwise |

<CheckoutSuccessSummary>

Displays a success banner after checkout. Parses Creem query params automatically.

| Prop | Type | Description | | ----------- | ----------------------- | ------------------------------------------------------------ | | params | CheckoutSuccessParams | Manual params (overrides URL parsing) | | search | string | Query string to parse (defaults to window.location.search) | | className | string | CSS class |

React also exports a useCheckoutSuccessParams() hook that returns the parsed params directly.

<TrialLimitBanner>

Shows a trial expiration notice.

| Prop | Type | Description | | ------------- | ------------------------- | ----------------------- | | snapshot | BillingSnapshot \| null | Current billing state | | trialEndsAt | string \| null | Override trial end date | | className | string | CSS class |

<ScheduledChangeBanner>

Shows a cancellation-scheduled notice with optional "Undo" button.

| Prop | Type | Description | | ----------- | ------------------------- | ------------------------------------------------- | | snapshot | BillingSnapshot \| null | Current billing state | | isLoading | boolean | Loading state for resume button | | onResume | () => void | Resume handler (shows "Undo cancellation" button) | | className | string | CSS class |

<PaymentWarningBanner>

Shows a warning for pending, refunded, or partially refunded payments.

| Prop | Type | Description | | ----------- | ------------------------- | --------------------- | | snapshot | BillingSnapshot \| null | Current billing state | | payment | PaymentSnapshot \| null | Override payment data | | className | string | CSS class |

<OneTimePaymentStatusBadge>

Inline status badge for one-time payments.

| Prop | Type | Description | | ----------- | ----------------------------------------------------------- | -------------- | | status | "pending" \| "paid" \| "refunded" \| "partially_refunded" | Payment status | | className | string | CSS class |


Troubleshooting

Webhooks not receiving events Verify your Creem dashboard webhook URL matches <CONVEX_SITE_URL>/creem/events. Check that CREEM_WEBHOOK_SECRET matches the signing secret in Creem. Check the Convex dashboard logs for verification errors.

Products not syncing Run npx convex run billing:syncBillingProducts after setting up webhooks. Ensure CREEM_API_KEY is set and the key has read access to products.

Widgets rendering unstyled Ensure both Tailwind CSS v4 and @import "@mmailaender/convex-creem/styles" are in your CSS entry point. The styles import must come after the Tailwind import.

Checkout URL missing from response The Creem API returned no checkout URL. Verify the product ID exists and is active in your Creem dashboard. Check the Convex dashboard logs for the full error.

Entity/org billing not scoping correctly Ensure billingEntityId is returned from getUserInfo. If omitted, userId is used as the billing entity. Verify that checkout metadata includes convexBillingEntityId by checking webhook logs.

"Customer not found" when opening billing portal The customer record is created on first checkout. If the user hasn't completed a checkout yet, there's no customer to link to the portal.