Where this code goes, and what it assumes
Where this code goes, and what it assumes
Examples on this page are fragments. They render inside the provider you mounted
in the Quickstart:If something below does not work, one of these is usually missing. Each links
straight to the step that sets it up.
- Component registered —
app.use(creem)inconvex/convex.config.ts - Secrets set —
CREEM_API_KEYandCREEM_WEBHOOK_SECRETin Convex env - Billing API exported —
convex/billing.tscallingcreem.api({ resolve }) - Webhook registered —
creem.registerRoutes(http)inconvex/http.ts - Products created — one Creem product per plan and billing cycle
- Products synced —
npx convex run billing:syncBillingProducts - Styles imported —
@creem_io/convex/stylesafter the Tailwind import - Catalog defined —
billingCatalogmapping plan IDs to product IDs - Provider mounted —
<CreemConvexProvider>around your billing UI
Custom auth and RBAC
Billing and identity are separate domains that meet in one place: theresolve
callback. Your auth layer answers who is calling and what may they act on.
This component answers what that billing entity owns and may change. resolve
is the handover between the two.
On the billing side, the generated API enforces its own boundary: it derives the
entity from resolve, scopes customer data to that entity, and verifies
explicitly selected subscriptions before reading or changing them. It never
takes an entity from client input.
On the identity side, sessions, roles, and org membership stay with whatever auth
you already use. Convex Auth, Clerk, WorkOS, or your own tables all work the
same way here, because the component only ever sees the result.
Quick start
creem.api({ resolve }) generates ready-to-export Convex functions. Each one
calls your resolve callback to authenticate and determine the entityId. For
team billing, verify membership and the required billing role before returning
an organization ID, since that check belongs to the identity domain. This is
what the Quickstart uses.
Full control
Call the resource namespaces (creem.subscriptions.*, creem.checkouts.*,
creem.customers.*, creem.orders.*, creem.products.*) inside your own Convex
functions and handle auth, entity resolution, and permission checks there. The
library exports shared arg validators matching exactly what the widgets send,
so your custom functions stay drop-in compatible:
subscriptionUpdateArgs carries a kind discriminator for the three mutually exclusive update targets. The exported SubscriptionUpdateArgs type is a true discriminated union:
"immediate" on a paid switch, or two targets at once, does not compile.
Convex requires top-level args to be a flat object, which means the wire validator itself is permissive. The generated mutation calls parseSubscriptionUpdateArgs to re-check the same rules for calls arriving over the wire. Use it in your own wrappers too:
convex/billing.ts
permissions so non-admins see disabled buttons instead of server errors. The server check above is what actually enforces the rule.
Credit grants and spending are deliberately not part of ConnectedBillingApi.
Expose an app-specific backend action for a business operation such as
generateImage, and let that action call creem.credits.creditForEntity or
creem.credits.debitForEntity with server-controlled amounts, references, and
idempotency keys.
Resolver plan overrides
The resolver may return two optional overrides, for apps that own their plan assignment rather than letting the component own it:
Omit both and the component reads the active plan from its own app-plan
assignment rows.
Webhook event middleware
registerRoutes accepts an events map for app-specific logic. Your handlers run after the component’s built-in processing, which upserts customers, subscriptions, and orders.
The ctx is a Convex action context. It has no ctx.db, so reach your own tables through ctx.runQuery and ctx.runMutation, and use ctx.runAction for third-party calls:
checkout.completed, subscription.active, subscription.paid, subscription.canceled, subscription.scheduled_cancel, subscription.past_due, subscription.expired, subscription.trialing, subscription.paused, subscription.unpaid, subscription.update, refund.created, dispute.created. Dispute events are available to custom handlers only (no built-in sync).
Checkout gates and auto-resume
onBeforeCheckout fires before the widget calls checkouts.create, at either the provider or the widget level. Return false to abort. Use it for auth gates, terms acceptance, confirmation dialogs, or analytics:
pendingCheckout.save(intent), the widget notices when the Convex query re-fires with an authenticated user and re-triggers checkout itself. This works for modal auth such as Clerk or an Auth0 popup, and for redirect auth such as OAuth, with no manual resume code.
Auto-resume is skipped when the user already has an active subscription or owns the product, so a sign-in cannot produce a duplicate purchase.
Sibling guards for the other flows: onBeforePlanChange (paid switches and unit updates) and onBeforePlanActivation (app-owned plans like trials).
Internationalization
Seti18n once on the provider. Every connected widget inherits it, covering
cards, dialogs, billing history, portal buttons, recovery banners, credits, and
accessibility labels:
Custom billing UI model
uiModel returns everything the widgets need. To add app-specific fields, write your own query on top of creem.getBillingModel():
entityId is handled gracefully, so public pricing pages get the catalog without auth.