Stripe
Checkout, subscriptions and billing
git clone https://github.com/bijectionhq/stripe.git
cd stripe
git checkout 22e23e2bdfcc88b48a1604a80bf60d8f15494360Bijection repository access is required. Request access.
Description
Checkout, subscriptions and billing
Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.
This package has a pinned source revision in Bijection’s workspace. 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: bijectionhq/stripe. Based on convex/stripe. License: Apache-2.0. Consult the repository for license terms, notices, and contributor attribution.
From the upstream publisher
A stripe component for Convex.
Explore the demo ↗Demo video
Upstream overview
From the upstream package documentation.
A Convex component that handles the complete Stripe integration lifecycle including checkout sessions, subscription management, customer billing portals, and webhook synchronization. It automatically creates and manages customers, processes payments and subscriptions, and keeps Stripe data synchronized with your Convex database through webhook handlers. The component provides both a StripeSubscriptions client API and direct database queries for real-time payment data.
Use cases
From the upstream package documentation.
- SaaS subscription billing - Implement recurring billing with seat-based pricing, subscription upgrades/downgrades, and automatic customer portal access for plan management
- One-time payment processing - Handle product purchases, service fees, or donations with Stripe Checkout integration and payment status tracking
- Team billing management - Link subscriptions to organizations with quantity-based pricing, allowing teams to add or remove seats dynamically
- Customer self-service billing - Generate Customer Portal sessions so users can update payment methods, download invoices, and manage their billing without support
- Multi-tenant payment tracking - Query payments, subscriptions, and invoices in real-time across users and organizations with automatic webhook synchronization
How it works
From the upstream package documentation.
The component uses a StripeSubscriptions client that wraps Stripe API calls within Convex actions, handling customer creation, checkout sessions, and subscription management. You configure it by adding the component to your convex.config.ts and setting up webhook routes in http.ts that point to the component's webhook handler at /stripe/webhook.
Webhook events automatically synchronize Stripe data to dedicated Convex tables (customers, subscriptions, payments, invoices) in the component's namespace. The component handles 10+ webhook events including checkout.session.completed, customer.subscription.updated, and payment_intent.succeeded, maintaining real-time data consistency.
You access payment data through the component's public queries like listSubscriptionsByUserId and getPayment, or use the client methods like createCheckoutSession and updateSubscriptionQuantity in your actions. The component supports linking payments to both individual users and organizations through metadata, enabling flexible billing models.
Upstream package FAQ
Does this component handle Stripe webhook verification?
Yes, the @convex-dev/stripe component automatically verifies webhook signatures using your Stripe webhook secret. It validates all incoming webhooks and rejects unauthorized requests, ensuring secure payment processing in your Convex application.
Can I customize which Stripe events are processed?
The @convex-dev/stripe component allows you to configure which Stripe events trigger database updates. You can enable or disable specific event handlers and add custom logic for processing payments, subscriptions, and customer data according to your application needs.
How does this integrate with Convex's real-time features?
When Stripe webhooks update your database through @convex-dev/stripe, all connected clients receive real-time updates via Convex's reactive queries. This means payment confirmations, subscription changes, and billing updates instantly reflect across your application without manual polling.
What Stripe data gets stored in my Convex database?
The @convex-dev/stripe component stores essential payment and subscription data in your Convex database, including customer IDs, payment statuses, subscription tiers, and billing cycles. You control which fields are saved and can extend the schema to include additional Stripe metadata for your application.
Use with agents and CLI
Give your coding agent the component’s source identity and setup guidance.
Help me integrate Stripe with Bijection.
Read: https://bijection.com/components/stripe/SKILL.md
Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.Upstream README.md
View repository ↗Documentation from convex/stripe, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.
@convex-dev/stripe
A Convex component for integrating Stripe payments, subscriptions, and billing into your Convex application.
Requires Node.js 18 or later, matching the Stripe Node SDK v22 runtime requirement.
Features
- 🛒 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
Quick Start
1. Install the Component
npm install @convex-dev/stripe
2. Add to Your Convex App
Create or update 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;
3. Set Up Environment Variables
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 |
4. Configure Stripe Webhooks
- 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:
checkout.session.completedcustomer.createdcustomer.updatedcustomer.deletedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.createdinvoice.finalizedinvoice.updatedinvoice.paidinvoice.payment_failedpayment_intent.succeededpayment_intent.payment_failed
- Click "Add endpoint"
- Copy the Signing secret and add it as
STRIPE_WEBHOOK_SECRETin Convex
5. Register Webhook Routes
Create convex/http.ts:
import { httpRouter } from "convex/server";
import { components } from "./_generated/api";
import { registerRoutes } from "@convex-dev/stripe";
const http = httpRouter();
// Register Stripe webhook handler at /stripe/webhook
registerRoutes(http, components.stripe, {
webhookPath: "/stripe/webhook",
apiVersion: "2026-04-22.dahlia", // Optional
});
export default http;
6. Use the Component
Create convex/stripe.ts:
import { action } 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, {});
// 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 },
});
},
});
API Reference
StripeSubscriptions Client
import { StripeSubscriptions } from "@convex-dev/stripe";
const stripeClient = new StripeSubscriptions(components.stripe, {
STRIPE_SECRET_KEY: "sk_...", // Optional, defaults to process.env.STRIPE_SECRET_KEY
apiVersion: "2026-04-22.dahlia", // Optional Stripe API version
});
Methods
| 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 |
createCheckoutSession
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
params: {
allow_promotion_codes: true,
ui_mode: "embedded_page",
return_url: "https://...",
}, // Optional Stripe Checkout Session params
});
params is a typed passthrough for Stripe Checkout Session fields that the
component does not model directly. Fields in params override constructed
defaults, except mode, which remains controlled by the top-level mode
argument. For non-hosted Checkout UI modes, the component omits successUrl and
cancelUrl from the Stripe request and expects redirect behavior to be supplied
through Stripe-supported params such as return_url.
Component Queries
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,
});
},
});
Available Public Queries
| 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 |
getSubscription | stripeSubscriptionId | Get a subscription by ID |
getSubscriptionByOrgId | orgId | Get subscription for an org |
getPayment | stripePaymentIntentId | Get a payment by ID |
listPayments | stripeCustomerId | List payments for a customer |
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 |
Webhook Events
The component automatically handles these Stripe webhook events:
| Event | Action |
|---|---|
customer.created | Creates customer record |
customer.updated | Updates customer record |
customer.deleted | Scrubs customer PII |
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.finalized | Upserts invoice record |
invoice.updated | Mirrors invoice metadata changes |
invoice.paid | Upserts invoice as paid |
invoice.payment_failed | Marks invoice as failed |
checkout.session.completed | Handles completed checkout sessions |
Custom Webhook Handlers
Add custom logic to webhook events:
import { httpRouter } from "convex/server";
import { components } from "./_generated/api";
import { registerRoutes } from "@convex-dev/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;
Database Schema
The component creates these tables in its namespace:
customers
| Field | Type | Description |
|---|---|---|
stripeCustomerId | string | Stripe customer ID |
email | string? | Customer email |
name | string? | Customer name |
metadata | object? | Custom metadata |
subscriptions
| 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 |
checkout_sessions
| Field | Type | Description |
|---|---|---|
stripeCheckoutSessionId | string | Checkout session ID |
stripeCustomerId | string? | Customer ID |
status | string | Session status |
mode | string | Session mode (payment/subscription/setup) |
metadata | object? | Custom metadata |
payments
| 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 |
invoices
| 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 |
metadata | object? | Invoice metadata |
Example App
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 dev
The 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
Authentication
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",
},
],
};
Troubleshooting
Tables are empty after checkout
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)
"Not authenticated" errors
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
Webhooks returning 400/500
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
License
Apache-2.0