Kinde Billing
Add Kinde billing to your Convex app. Reactive subscriptions, checkout, self-serve portal, and feature gating.
git clone https://github.com/sholajegede/convex-kinde-billing.git
cd convex-kinde-billingThis upstream package has not been adapted or qualified for Bijection. Read the original documentation before adopting it.
Description
Add Kinde billing to your Convex app. Reactive subscriptions, checkout, self-serve portal, and feature gating.
Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.
Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.
Getting started
- Get the source at the revision above and read its package documentation.
- Check the required Bijection SDK version, dependencies, and external service credentials.
- Follow the package setup instructions and the guide to using components.
- Exercise the component in your deployment according to your project’s verification rules.
Open source
Maintained source: sholajegede/convex-kinde-billing. License: See repository. Consult the repository for license terms, notices, and contributor attribution.
From the upstream publisher
Sync Kinde billing events into Convex reactively. Real-time subscription state, payment tracking, and metered usage.
Explore the demo ↗Upstream overview
From the upstream package documentation.
Kinde Billing integrates Kinde's subscription billing with Convex through webhook handling and reactive queries. It automatically syncs subscription state, plan assignments, and billing events from Kinde webhooks into your Convex database. Provides feature gating functions like hasActivePlan() and hasFeature(), plus checkout URL generation and self-serve billing portal access.
Use cases
From the upstream package documentation.
- SaaS subscription management - Gate premium features based on active subscription status and automatically sync billing state changes without manual webhook handling
- Feature flagging by plan - Check if users have access to specific features using plan-based permissions with reactive queries that update instantly when subscriptions change
- Checkout flow implementation - Generate Kinde-hosted checkout URLs with pre-selected plans and redirect users to complete payment without building custom billing forms
- Self-service billing portal - Provide customers with one-click access to manage their subscriptions, update payment methods, and view billing history through Kinde's hosted portal
- Usage-based billing tracking - Store and query metered usage records in real-time for API calls, storage, or other measurable features tied to subscription limits
How it works
From the upstream package documentation.
The component handles Kinde billing webhooks through a mounted HTTP handler in convex/http.ts that processes subscription events like plan assignments, payment successes, and cancellations. All billing state gets stored in Convex tables with automatic JWT verification and real-time reactivity.
Key functions include hasActivePlan() and hasFeature() for subscription gating, getActivePlan() for current subscription details, and listBillingEvents() for billing history. The getCheckoutUrl() method generates Kinde-hosted checkout links with pre-selected plans, while getPortalUrl() creates one-time billing portal URLs using the user's access token.
Setup requires mounting the webhook handler, configuring the KINDE_ISSUER_URL environment variable, and registering the webhook endpoint in Kinde's dashboard. The component automatically handles webhook timing delays and provides instant UI updates through Convex's reactive queries once billing events arrive.
Upstream package FAQ
How does convex-kinde-billing sync billing data?
The convex-kinde-billing component receives Kinde webhook events through a mounted HTTP handler and automatically stores subscription status, billing events, and usage records in Convex. This data becomes available through reactive queries that update in real-time when billing changes occur.
What billing events does this component handle?
The convex-kinde-billing component handles all 8 Kinde billing webhook events including customer.plan_assigned, customer.agreement_created, customer.subscription_updated, customer.payment_succeeded, and customer.subscription_cancelled. Each event updates the corresponding Convex database records automatically.
Can I query metered usage data with this component?
Yes, convex-kinde-billing stores and provides access to metered usage records through the getUsage() method. You can query usage by customer ID and meter ID, with results returned in real-time as new usage events arrive via webhooks.
How do I gate features by subscription plan?
Use kindeBilling.hasActivePlan() to check if a customer has any active subscription, or kindeBilling.hasFeature() to check for specific plan features. Both methods work in Convex queries and mutations and automatically update when subscription status changes through webhook events.
Use with agents and CLI
Give your coding agent the component’s source identity and setup guidance.
Help me integrate Kinde Billing with Bijection.
Read: https://bijection.com/components/convex-kinde-billing/SKILL.md
Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.Upstream README.md
View repository ↗Documentation from sholajegede/convex-kinde-billing, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.
convex-kinde-billing
Add Kinde billing to your Convex app. Reactive subscriptions, checkout, self-serve portal, and feature gating.
const kindeBilling = new KindeBilling(components.convexKindeBilling, {
KINDE_ISSUER_URL: process.env.KINDE_ISSUER_URL!,
});
// Is this user on an active plan?
const active = await kindeBilling.hasActivePlan(ctx, { customerId: "customer_abc123" });
// Does this user have access to a specific feature?
const hasAccess = await kindeBilling.hasFeature(ctx, { customerId: "customer_abc123", featureKey: "pro" });
// Generate a checkout URL to send users to Kinde's hosted billing
const checkoutUrl = kindeBilling.getCheckoutUrl({
clientId: process.env.KINDE_CLIENT_ID!,
redirectUri: "https://yourapp.com/dashboard",
planKey: "customer_pro_plan",
});
// Generate a one-time self-serve billing portal link (using user's access token)
const portalUrl = await kindeBilling.getPortalUrl(userAccessToken, {
returnUrl: "https://yourapp.com/settings",
});
What this does
Kinde fires webhook events every time a billing action occurs — plan assigned, payment succeeded, subscription cancelled. Without this component, you have to write and maintain your own webhook handler, JWT verification, database schema, and reactive queries.
This component owns all of that. Drop it in, mount the webhook, and your Convex app immediately has:
- Reactive billing state — subscription status, plan name, renewal date, live in Convex
- Feature gating —
hasFeature()andhasActivePlan()to gate any feature or route - Checkout —
getCheckoutUrl()generates a Kinde-hosted checkout link with plan pre-selected - Self-serve portal —
getPortalUrl()generates a one-time billing portal URL using the user's access token. - Metered usage — usage records stored and queryable in real time
- React component —
<ManageBillingButton>drop-in portal button
Webhook timing: After a billing action occurs in Kinde, there is a short delay — usually a few seconds — before the webhook arrives and your Convex data updates. Once the webhook arrives, Convex's real-time reactivity propagates the change to all subscribers instantly.
Table of Contents
- Install
- Quick Start
- Setup
- Usage
- Checkout
- Self-Serve Portal
- React Components
- API Reference
- Type Reference
- Webhook Events
- Database Schema
- Customer IDs
- Using with kinde-sync
- Testing
- Limitations
- Troubleshooting
- Contributing
- Changelog
Install
npm install convex-kinde-billing
Requirements: Convex v1.33.1 or later, Node.js 18+
Quick Start
Five steps to add Kinde billing to your Convex app.
1. Add the component
In convex/convex.config.ts:
import { defineApp } from "convex/server";
import convexKindeBilling from "convex-kinde-billing/convex.config";
const app = defineApp();
app.use(convexKindeBilling);
export default app;
2. Set environment variables
npx convex env set KINDE_ISSUER_URL https://yourdomain.kinde.com
3. Mount the webhook handler
In convex/http.ts:
import { httpRouter } from "convex/server";
import { components } from "./_generated/api";
import { KindeBilling } from "convex-kinde-billing";
const kindeBilling = new KindeBilling(components.convexKindeBilling, {
KINDE_ISSUER_URL: process.env.KINDE_ISSUER_URL!,
});
const http = httpRouter();
http.route({
path: "/webhooks/kinde/billing",
method: "POST",
handler: kindeBilling.webhookHandler,
});
export default http;
4. Register the webhook in Kinde
- In Kinde → Webhooks → Add endpoint
- Set the URL:
https://your-deployment.convex.site/webhooks/kinde/billing - Select all 8 billing events (listed in Webhook Events below)
- Save
Your Convex site URL is in the Convex dashboard under Settings → URL & Deploy Key — it ends in .convex.site.
5. Initialize the client
In convex/billing.ts:
import { components } from "./_generated/api";
import { KindeBilling } from "convex-kinde-billing";
export const kindeBilling = new KindeBilling(components.convexKindeBilling, {
KINDE_ISSUER_URL: process.env.KINDE_ISSUER_URL!,
});
Import kindeBilling from this file in any Convex function that needs billing.
Setup
convex/billing.ts — your central billing module:
import { components } from "./_generated/api";
import { KindeBilling } from "convex-kinde-billing";
import { query } from "./_generated/server";
import { v } from "convex/values";
export const kindeBilling = new KindeBilling(components.convexKindeBilling, {
KINDE_ISSUER_URL: process.env.KINDE_ISSUER_URL!,
});
export const checkAccess = query({
args: { customerId: v.string() },
handler: async (ctx, { customerId }) =>
kindeBilling.hasActivePlan(ctx, { customerId }),
});
convex/http.ts — webhook entry point:
import { httpRouter } from "convex/server";
import { components } from "./_generated/api";
import { KindeBilling } from "convex-kinde-billing";
const kindeBilling = new KindeBilling(components.convexKindeBilling, {
KINDE_ISSUER_URL: process.env.KINDE_ISSUER_URL!,
});
const http = httpRouter();
http.route({
path: "/webhooks/kinde/billing",
method: "POST",
handler: kindeBilling.webhookHandler,
});
export default http;
Usage
Check if a customer has an active plan
export const checkAccess = query({
args: { customerId: v.string() },
handler: async (ctx, args) => {
return await kindeBilling.hasActivePlan(ctx, { customerId: args.customerId });
},
});
// Returns: true | false
// Returns false (never throws) when customerId doesn't exist yet
Check if a customer has a specific feature
export const checkFeature = query({
args: { customerId: v.string(), featureKey: v.string() },
handler: async (ctx, args) => {
return await kindeBilling.hasFeature(ctx, args);
},
});
// Returns: true if customer is active and planId or planName contains featureKey
Get the customer's current plan
export const getPlan = query({
args: { customerId: v.string() },
handler: async (ctx, args) => {
return await kindeBilling.getActivePlan(ctx, { customerId: args.customerId });
},
});
// Returns: { planId, planName, status, currentPeriodEnd } | null
Get the full subscription record
export const getSubscription = query({
args: { customerId: v.string() },
handler: async (ctx, args) => {
return await kindeBilling.getSubscription(ctx, { customerId: args.customerId });
},
});
// Returns: Subscription | null
List billing events for a customer
export const getBillingHistory = query({
args: { customerId: v.string() },
handler: async (ctx, args) => {
return await kindeBilling.listBillingEvents(ctx, {
customerId: args.customerId,
limit: 20,
});
},
});
// Returns: BillingEvent[] ordered newest first
Query metered usage records
export const getApiUsage = query({
args: { customerId: v.string() },
handler: async (ctx, args) => {
return await kindeBilling.getUsage(ctx, {
customerId: args.customerId,
meterId: "api_calls",
limit: 100,
});
},
});
// Returns: UsageRecord[] ordered newest first
Gate a feature by plan status
export const generateReport = action({
args: { userId: v.string() },
handler: async (ctx, { userId }) => {
const active = await kindeBilling.hasActivePlan(ctx, { customerId: userId });
if (!active) throw new Error("Upgrade required to generate reports.");
// ... generate report
},
});
Gate by plan name
export const accessAdvancedAnalytics = query({
args: { customerId: v.string() },
handler: async (ctx, { customerId }) => {
const plan = await kindeBilling.getActivePlan(ctx, { customerId });
if (!plan || plan.planName !== "Pro") {
return { allowed: false, reason: "Pro plan required" };
}
return { allowed: true };
},
});
Checkout
Kinde's checkout is hosted — you redirect users to a Kinde URL and they complete payment there. getCheckoutUrl() builds that URL with the plan pre-selected.
const checkoutUrl = kindeBilling.getCheckoutUrl({
clientId: process.env.KINDE_CLIENT_ID!,
redirectUri: "https://yourapp.com/dashboard",
planKey: "customer_pro_plan",
});
window.location.href = checkoutUrl;
Show a pricing table instead of pre-selecting a plan:
const checkoutUrl = kindeBilling.getCheckoutUrl({
clientId: process.env.KINDE_CLIENT_ID!,
redirectUri: "https://yourapp.com/dashboard",
pricingTableKey: "main_pricing_table",
});
For B2B org sign-up:
const checkoutUrl = kindeBilling.getCheckoutUrl({
clientId: process.env.KINDE_CLIENT_ID!,
redirectUri: "https://yourapp.com/dashboard",
planKey: "customer_pro_plan",
isCreateOrg: true,
});
After checkout, Kinde fires customer.plan_assigned and customer.agreement_created webhooks — your Convex billing state updates automatically.
Self-Serve Portal
getPortalUrl() calls Kinde's Account API using the logged-in user's access token.
Using Kinde's React SDK
import { PortalLink } from "@kinde-oss/kinde-auth-react";
<PortalLink>Manage Billing</PortalLink>
Without Kinde's React SDK
// Get the user's Kinde access token from your auth flow, then:
const portalUrl = await kindeBilling.getPortalUrl(userAccessToken, {
returnUrl: "https://yourapp.com/settings",
});
window.location.href = portalUrl;
For B2B org billing portal:
const portalUrl = await kindeBilling.getPortalUrl(userAccessToken, {
returnUrl: "https://yourapp.com/settings",
subNav: "organization_billing",
});
window.location.href = portalUrl;
React Components
<PortalLink> — Kinde Auth SDK
If you use @kinde-oss/kinde-auth-react, use Kinde's built-in component. No backend call needed:
import { PortalLink } from "@kinde-oss/kinde-auth-react";
<PortalLink>Manage Billing</PortalLink>
ManageBillingButton
Drop-in button that calls getPortalUrl using the user's access token and redirects to the portal.
import { ManageBillingButton } from "convex-kinde-billing/react";
function SettingsPage({ userAccessToken }: { userAccessToken: string }) {
return (
<ManageBillingButton
kindeBilling={kindeBilling}
userAccessToken={userAccessToken}
returnUrl={window.location.href}
>
Manage Billing
</ManageBillingButton>
);
}
Props:
| Prop | Type | Required | Description |
|||||
| kindeBilling | KindeBilling | ✓ | Your KindeBilling instance |
| userAccessToken | string | ✓ | The logged-in user's Kinde access token |
| returnUrl | string | | URL to return to after the portal |
| children | ReactNode | | Button label (default: "Manage Billing") |
| className | string | | CSS class name |
API Reference
KindeBilling class
import { KindeBilling } from "convex-kinde-billing";
const kindeBilling = new KindeBilling(components.convexKindeBilling, {
KINDE_ISSUER_URL: process.env.KINDE_ISSUER_URL!,
});
Constructor options
| Option | Type | Required | Description |
|||||
| KINDE_ISSUER_URL | string | ✓ | Your Kinde issuer URL e.g. https://yourdomain.kinde.com |
Methods
| Method | Type | Args | Returns | Description |
||||||
| webhookHandler | HTTP action | — | — | Mount in convex/http.ts to receive Kinde billing webhooks |
| getCheckoutUrl | sync | { clientId, redirectUri, planKey?, pricingTableKey?, isCreateOrg? } | string | Build a Kinde-hosted checkout URL |
| getPortalUrl | async | `(userAcce
[README truncated for prompt length]