| name | convex-billing |
| description | Add Stripe billing/payments to the Convex app via @convex-dev/stripe (checkout + webhook + gating). |
Add billing / payments
Wire Stripe to Convex using @convex-dev/stripe: a checkout action, an httpAction webhook registered by the component (signature-verified automatically), subscription state stored in the component's tables, and server-side gating via a query.
Workflow
- Install the component:
npm install @convex-dev/stripe.
- Create
convex/convex.config.ts:
import { defineApp } from "convex/server";
import stripe from "@convex-dev/stripe/convex.config.js";
const app = defineApp();
app.use(stripe);
export default app;
- Store Stripe keys in Convex env (use the
env micro power): STRIPE_SECRET_KEY (sk_test_… / sk_live_…) and STRIPE_WEBHOOK_SECRET (whsec_…).
- Create
convex/http.ts to register the webhook route (the component handles signature verification automatically):
import { httpRouter } from "convex/server";
import { components } from "./_generated/api";
import { registerRoutes } from "@convex-dev/stripe";
const http = httpRouter();
registerRoutes(http, components.stripe, { webhookPath: "/stripe/webhook" });
export default http;
- Create
convex/billing.ts with a checkout action and a subscription-gate query:
import { action, query } from "./_generated/server";
import { components } from "./_generated/api";
import { StripeSubscriptions } from "@convex-dev/stripe";
import { v } from "convex/values";
const stripeClient = new StripeSubscriptions(components.stripe, {});
export const createSubscriptionCheckout = action({
args: { priceId: v.string() },
returns: v.object({ sessionId: v.string(), url: v.union(v.string(), v.null()) }),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
const customer = await stripeClient.getOrCreateCustomer(ctx, {
userId: identity.subject,
email: identity.,
: identity.,
});
stripeClient.(ctx, {
: args.,
: customer.,
: ,
: ,
: ,
: { : identity. },
});
},
});
isSubscribed = ({
: {},
: v.(),
: (ctx) => {
identity = ctx..();
(!identity) ;
subscriptions = ctx.(
components...,
{ : identity. },
);
subscriptions.( sub. === || sub. === );
},
});
- Run
npx convex dev --once — it will install the component and push the functions. Verify output shows ✔ Installed component stripe.
- In Stripe Dashboard → Webhooks: add endpoint
https://<deployment>.convex.site/stripe/webhook, subscribe to checkout.session.completed, customer.subscription.*, invoice.*, payment_intent.*. Copy the signing secret as STRIPE_WEBHOOK_SECRET.
Rules
- Use @convex-dev/stripe (npm: @convex-dev/stripe@^0.1.4) — it handles webhook signature verification internally via registerRoutes; do NOT write a manual constructEvent webhook.
- Stripe keys live in Convex env (use the
env micro power): STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET.
- Gate on server-stored subscription state via isSubscribed query (reads component tables), not client claims.
- convex/convex.config.ts must import from '@convex-dev/stripe/convex.config.js' (not .ts) — the .js extension is required by the Convex bundler.