A Convex component for integrating Stripe payments, subscriptions, and billing into your Convex application.
- đź›’ Checkout Sessions - Create one-time payment and subscription checkouts
- 📦 Subscription Management - Create, update, cancel subscriptions
- 👥 Customer Management - Automatic customer creation and linking
- đź’ł Customer Portal - Let users manage their billing
- 🪑 Seat-Based Pricing - Update subscription quantities for team billing
- đź”— User/Org Linking - Link payments and subscriptions to users or organizations
- đź”” Webhook Handling - Automatic sync of Stripe data to your Convex database
- 📊 Real-time Data - Query payments, subscriptions, invoices in real-time
npm install @convex/stripeCreate or update convex/convex.config.ts:
import { defineApp } from "convex/server";
import stripe from "@convex/stripe/convex.config.js";
const app = defineApp();
app.use(stripe);
export default app;Add these to your Convex Dashboard → Settings → Environment Variables:
| Variable | Description |
|---|---|
STRIPE_SECRET_KEY |
Your Stripe secret key (sk_test_... or sk_live_...) |
STRIPE_WEBHOOK_SECRET |
Webhook signing secret (whsec_...) - see Step 4 |
- Go to Stripe Dashboard → Developers → Webhooks
- Click "Add endpoint"
- Enter your webhook URL:
(Find your deployment name in the Convex dashboard - it's the part before
https://<your-convex-deployment>.convex.site/stripe/webhook.convex.cloudin your URL) - Select these events:
customer.createdcustomer.updatedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedpayment_intent.succeededpayment_intent.payment_failedinvoice.createdinvoice.finalizedinvoice.paidinvoice.payment_failedcheckout.session.completed
- Click "Add endpoint"
- Copy the Signing secret and add it as
STRIPE_WEBHOOK_SECRETin Convex
Create convex/http.ts:
import { httpRouter } from "convex/server";
import { components } from "./_generated/api";
import { registerRoutes } from "@convex/stripe";
const http = httpRouter();
// Register Stripe webhook handler at /stripe/webhook
registerRoutes(http, components.stripe, {
webhookPath: "/stripe/webhook",
});
export default http;Create convex/stripe.ts:
import { action } from "./_generated/server";
import { components } from "./_generated/api";
import { StripeSubscriptions } from "@convex/stripe";
import { v } from "convex/values";
const stripeClient = new StripeSubscriptions(components.stripe, {});
// Create a checkout session for a subscription
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");
// Get or create a Stripe customer
const customer = await stripeClient.getOrCreateCustomer(ctx, {
userId: identity.subject,
email: identity.email,
name: identity.name,
});
// Create checkout session
return await stripeClient.createCheckoutSession(ctx, {
priceId: args.priceId,
customerId: customer.customerId,
mode: "subscription",
successUrl: "http://localhost:5173/?success=true",
cancelUrl: "http://localhost:5173/?canceled=true",
subscriptionMetadata: { userId: identity.subject },
});
},
});
// Create a checkout session for a one-time payment
export const createPaymentCheckout = 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.email,
name: identity.name,
});
return await stripeClient.createCheckoutSession(ctx, {
priceId: args.priceId,
customerId: customer.customerId,
mode: "payment",
successUrl: "http://localhost:5173/?success=true",
cancelUrl: "http://localhost:5173/?canceled=true",
paymentIntentMetadata: { userId: identity.subject },
});
},
});import { StripeSubscriptions } from "@convex/stripe";
const stripeClient = new StripeSubscriptions(components.stripe, {
STRIPE_SECRET_KEY: "sk_...", // Optional, defaults to process.env.STRIPE_SECRET_KEY
});| Method | Description |
|---|---|
createCheckoutSession() |
Create a Stripe Checkout session |
createCustomerPortalSession() |
Generate a Customer Portal URL |
createCustomer() |
Create a new Stripe customer |
getOrCreateCustomer() |
Get existing or create new customer |
cancelSubscription() |
Cancel a subscription |
reactivateSubscription() |
Reactivate a subscription set to cancel |
updateSubscriptionQuantity() |
Update seat count |
await stripeClient.createCheckoutSession(ctx, {
priceId: "price_...",
customerId: "cus_...", // Optional
mode: "subscription", // "subscription" | "payment" | "setup"
successUrl: "https://...",
cancelUrl: "https://...",
quantity: 1, // Optional, default 1
metadata: {}, // Optional, session metadata
subscriptionMetadata: {}, // Optional, attached to subscription
paymentIntentMetadata: {}, // Optional, attached to payment intent
});Access data directly via the component's public queries:
import { query } from "./_generated/server";
import { components } from "./_generated/api";
// List subscriptions for a user
export const getUserSubscriptions = query({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return [];
return await ctx.runQuery(
components.stripe.public.listSubscriptionsByUserId,
{ userId: identity.subject }
);
},
});
// List payments for a user
export const getUserPayments = query({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return [];
return await ctx.runQuery(
components.stripe.public.listPaymentsByUserId,
{ userId: identity.subject }
);
},
});| Query | Arguments | Description |
|---|---|---|
getCustomer |
stripeCustomerId |
Get a customer by Stripe ID |
listSubscriptions |
stripeCustomerId |
List subscriptions for a customer |
listSubscriptionsByUserId |
userId |
List subscriptions for a user |
listSubscriptionsByOrgId |
orgId |
List subscriptions for an org |
getSubscription |
stripeSubscriptionId |
Get a subscription by ID |
getSubscriptionByOrgId |
orgId |
Get subscription for an org |
listPaymentsByUserId |
userId |
List payments for a user |
listPaymentsByOrgId |
orgId |
List payments for an org |
listInvoices |
stripeCustomerId |
List invoices for a customer |
listInvoicesByUserId |
userId |
List invoices for a user |
listInvoicesByOrgId |
orgId |
List invoices for an org |
The component automatically handles these Stripe webhook events:
| Event | Action |
|---|---|
customer.created |
Creates customer record |
customer.updated |
Updates customer record |
customer.subscription.created |
Creates subscription record |
customer.subscription.updated |
Updates subscription record |
customer.subscription.deleted |
Marks subscription as canceled |
payment_intent.succeeded |
Creates payment record |
payment_intent.payment_failed |
Updates payment status |
invoice.created |
Creates invoice record |
invoice.paid |
Updates invoice to paid |
invoice.payment_failed |
Marks invoice as failed |
Add custom logic to webhook events:
import { httpRouter } from "convex/server";
import { components } from "./_generated/api";
import { registerRoutes } from "@convex/stripe";
import type Stripe from "stripe";
const http = httpRouter();
registerRoutes(http, components.stripe, {
events: {
"customer.subscription.updated": async (ctx, event: Stripe.CustomerSubscriptionUpdatedEvent) => {
const subscription = event.data.object;
console.log("Subscription updated:", subscription.id, subscription.status);
// Add custom logic here
},
},
onEvent: async (ctx, event: Stripe.Event) => {
// Called for ALL events - useful for logging/analytics
console.log("Stripe event:", event.type);
},
});
export default http;The component creates these tables in its namespace:
| Field | Type | Description |
|---|---|---|
stripeCustomerId |
string | Stripe customer ID |
email |
string? | Customer email |
name |
string? | Customer name |
metadata |
object? | Custom metadata |
| Field | Type | Description |
|---|---|---|
stripeSubscriptionId |
string | Stripe subscription ID |
stripeCustomerId |
string | Customer ID |
status |
string | Subscription status |
priceId |
string | Price ID |
quantity |
number? | Seat count |
currentPeriodEnd |
number | Period end timestamp |
cancelAtPeriodEnd |
boolean | Will cancel at period end |
userId |
string? | Linked user ID |
orgId |
string? | Linked org ID |
metadata |
object? | Custom metadata |
| Field | Type | Description |
|---|---|---|
stripePaymentIntentId |
string | Payment intent ID |
stripeCustomerId |
string? | Customer ID |
amount |
number | Amount in cents |
currency |
string | Currency code |
status |
string | Payment status |
created |
number | Created timestamp |
userId |
string? | Linked user ID |
orgId |
string? | Linked org ID |
metadata |
object? | Custom metadata |
| Field | Type | Description |
|---|---|---|
stripeInvoiceId |
string | Invoice ID |
stripeCustomerId |
string | Customer ID |
stripeSubscriptionId |
string? | Subscription ID |
status |
string | Invoice status |
amountDue |
number | Amount due |
amountPaid |
number | Amount paid |
created |
number | Created timestamp |
userId |
string? | Linked user ID |
orgId |
string? | Linked org ID |
Check out the full example app in the example/ directory:
git clone https://github.com/get-convex/convex-stripe
cd convex-stripe
npm install
npm run devThe example includes:
- Landing page with product showcase
- One-time payments and subscriptions
- User profile with order history
- Subscription management (cancel, update seats)
- Customer portal integration
- Team/organization billing
This component works with any Convex authentication provider. The example uses Clerk:
- Create a Clerk application at clerk.com
- Add
VITE_CLERK_PUBLISHABLE_KEYto your.env.local - Create
convex/auth.config.ts:
export default {
providers: [
{
domain: "https://your-clerk-domain.clerk.accounts.dev",
applicationID: "convex",
},
],
};Make sure you've:
- Set
STRIPE_SECRET_KEYandSTRIPE_WEBHOOK_SECRETin Convex environment variables - Configured the webhook endpoint in Stripe with the correct events
- Added
invoice.createdandinvoice.finalizedevents (not justinvoice.paid)
Ensure your auth provider is configured:
- Create
convex/auth.config.tswith your provider - Run
npx convex devto push the config - Verify the user is signed in before calling actions
Check the Convex logs in your dashboard for errors. Common issues:
- Missing
STRIPE_WEBHOOK_SECRET - Wrong webhook URL (should be
https://<deployment>.convex.site/stripe/webhook) - Missing events in webhook configuration
Apache-2.0