Skip to content

Find a component

Loading the directory…

↑ ↓ to select · Enter to open · Esc to closeBrowse all →
Contributor login

Kinde Billing

Add Kinde billing to your Convex app. Reactive subscriptions, checkout, self-serve portal, and feature gating.

Upstream source
git clone https://github.com/sholajegede/convex-kinde-billing.git
cd convex-kinde-billing

This 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.

Available to evaluate

Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.

Getting started

  1. Get the source at the revision above and read its package documentation.
  2. Check the required Bijection SDK version, dependencies, and external service credentials.
  3. Follow the package setup instructions and the guide to using components.
  4. 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.

agent-prompt.md
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() and hasActivePlan() 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

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

  1. In Kinde → Webhooks → Add endpoint
  2. Set the URL: https://your-deployment.convex.site/webhooks/kinde/billing
  3. Select all 8 billing events (listed in Webhook Events below)
  4. 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]

Keywords