Stripe syncing, subscriptions, checkouts, one time payments, billing portal and Stripe Connect for Convex apps. Implemented according to the best practices listed in Theo's Stripe Recommendations.
- Convex Stripe
npm install @raideno/convex-stripe stripe- Create a Stripe account.
- Configure a webhook endpoint pointing to:
https://<your-convex-app>.convex.site/stripe/webhook - Enable the required Stripe Events on the webhook.
- Enable the Stripe Billing Portal.
npx convex env set STRIPE_SECRET_KEY "<secret>"
npx convex env set STRIPE_ACCOUNT_WEBHOOK_SECRET "<secret>"If you plan to use Stripe Connect, also set:
npx convex env set STRIPE_CONNECT_WEBHOOK_SECRET "<secret>"Spread the stripeTables export into your Convex schema. This creates all the synced tables that the library uses to mirror Stripe data locally.
// convex/schema.ts
import { defineSchema } from "convex/server";
import { stripeTables } from "@raideno/convex-stripe/server";
export default defineSchema({
...stripeTables,
// your other tables...
});If you only want to sync specific tables and avoid creating empty tables in your database, you can use the allStripeTablesExcept or onlyStripeTables helpers instead of stripeTables:
// convex/schema.ts
import { defineSchema } from "convex/server";
import { onlyStripeTables } from "@raideno/convex-stripe/server";
export default defineSchema({
...onlyStripeTables(["stripeCustomers", "stripeSubscriptions", "stripeProducts", "stripePrices"]),
// your other tables...
});The package will only sync the tables you defined.
See Tables Reference for the full list of tables and their schemas.
Call internalConvexStripe with your Stripe credentials and sync configuration. This returns a stripe object with all the action functions, a store internal mutation and a sync internal action.
// convex/stripe.ts
import { internalConvexStripe } from "@raideno/convex-stripe/server";
import schema from "./schema";
export const { stripe, store, sync } = internalConvexStripe({
schema: schema,
stripe: {
secret_key: process.env.STRIPE_SECRET_KEY!,
account_webhook_secret: process.env.STRIPE_ACCOUNT_WEBHOOK_SECRET!,
},
});Note: All exposed actions (
store,sync) are internal. They can only be called from other Convex functions. Wrap them in public actions when needed.
Important:
storemust always be exported from the same file, as it is used internally by the library to persist webhook data.
Register the Stripe webhook and redirect routes on your Convex HTTP router. This sets up two routes:
POST /stripe/webhookto receive and verify Stripe webhook events.GET /stripe/return/*to handle post-checkout and post-portal redirect flows.
// convex/http.ts
import { httpRouter } from "convex/server";
import { stripe } from "./stripe";
const http = httpRouter();
stripe.addHttpRoutes(http);
export default http;Create a Stripe customer the moment a new entity (user, organization, etc.) is created in your app. An entityId is your app's internal identifier for the thing you are billing. Each entity must be associated with exactly one Stripe customer.
The customer can be created using stripe.customers.create. Below are examples using different auth providers, where the user is the entity being billed.
::: code-group
// convex/auth.ts
// example with convex-auth: https://labs.convex.dev/auth
import { convexAuth } from "@convex-dev/auth/server";
import { Password } from "@convex-dev/auth/providers/Password";
import { internal } from "./_generated/api";
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [Password],
callbacks: {
// NOTE: create a customer immediately after a user is created
afterUserCreatedOrUpdated: async (context, args) => {
await context.scheduler.runAfter(0, internal.stripe.createCustomer, {
entityId: args.userId,
email: args.profile.email,
});
},
},
});// convex/auth.ts
// example with better-auth: https://convex-better-auth.netlify.app/
// coming soon...// convex/auth.ts
// example with clerk: https://docs.convex.dev/auth/clerk
// coming soon...:::
When using the example above, you also need to export the createCustomer action from your Stripe file:
// convex/stripe.ts
import { v } from "convex/values";
import { internalAction } from "./_generated/server";
import { stripe } from "./stripe";
export const createCustomer = internalAction({
args: {
email: v.optional(v.string()),
entityId: v.string(),
},
handler: async (context, args) => {
return stripe.customers.create(context, {
email: args.email,
entityId: args.entityId,
});
},
});In your Convex project's dashboard, go to the Functions section and execute the sync action with { tables: true }.
This syncs already existing Stripe data (products, prices, customers, subscriptions, etc.) into your Convex database. It must be done in both your development and production deployments after installing or updating the library.
This step is not necessary if you are starting with a fresh, empty Stripe account.
With everything set up, you can now use the provided functions to:
- Create subscription checkout sessions:
stripe.subscribe. - Create one time payment checkout sessions:
stripe.pay. - Open the Stripe Billing Portal for an entity:
stripe.portal. - Create Stripe Connect accounts:
stripe.accounts.create. - Generate onboarding links for connected accounts:
stripe.accounts.link. - Query any of the synced tables directly in your Convex functions.
The internalConvexStripe function accepts a configuration object and an optional options object.
const { stripe, store, sync } = internalConvexStripe(configuration, options);The schema key holds your convex app schema, from it the package will infer which stripe tables should be synced or not depending on what stripeTables where defined.
The stripe key in the configuration object holds your Stripe credentials and API settings.
| Property | Type | Required | Description |
|---|---|---|---|
secret_key |
string |
Yes | Your Stripe secret key (starts with sk_). |
account_webhook_secret |
string |
Yes | The webhook signing secret for account level webhooks (starts with whsec_). |
connect_webhook_secret |
string |
No | The webhook signing secret for Stripe Connect webhooks. Required only if using Connect. |
version |
string |
No | Stripe API version to pin against. Defaults to 2025-08-27.basil. |
The sync key controls which tables are synced and allows you to define catalog items, webhook endpoints, and billing portal configuration.
The sync.catalog key lets you pre-define products and prices that should exist in Stripe. When the sync action is called with { catalog: true }, the library ensures these objects exist in your Stripe account.
internalConvexStripe({
// ...
sync: {
catalog: {
products: [
{ name: "Pro Plan", metadata: { convex_stripe_key: "pro" } },
],
prices: [
{
currency: "usd",
unit_amount: 1999,
recurring: { interval: "month" },
product_data: { name: "Pro Plan", metadata: { convex_stripe_key: "pro" } },
metadata: { convex_stripe_key: "pro-monthly" },
},
],
metadataKey: "convex_stripe_key", // default
behavior: {
onExisting: "update", // "update" | "archive_and_recreate" | "skip" | "error"
onMissingKey: "create", // "create" | "error"
},
},
},
});The sync.webhooks key lets you customize the metadata and description of the webhook endpoints that the library creates when you call sync with { webhooks: { account: true } } or { webhooks: { connect: true } }.
{
sync: {
webhooks: {
account: {
description: "My App - Account Webhook",
metadata: { app: "my-app" },
},
connect: {
description: "My App - Connect Webhook",
metadata: { app: "my-app" },
},
},
},
}The sync.portal key accepts a Stripe.BillingPortal.ConfigurationCreateParams object. When sync is called with { portal: true }, the library creates the billing portal configuration if it does not already exist.
The callbacks.afterChange function is called every time a row is inserted, upserted, or deleted in any of the synced Stripe tables. This is useful for triggering side effects when Stripe data changes.
internalConvexStripe({
// ...
callbacks: {
afterChange: async (context, operation, event) => {
// operation: "upsert" | "delete" | "insert"
// event.table: the name of the table that changed (e.g. "stripeSubscriptions")
console.log(`Stripe data changed: ${operation} on ${event.table}`);
},
},
});You can register additional webhook handlers to react to specific Stripe events beyond the default syncing behavior. Use defineWebhookHandler to create a handler:
import { defineWebhookHandler } from "@raideno/convex-stripe/server";
const myHandler = defineWebhookHandler({
events: ["invoice.payment_succeeded"],
handle: async (event, context, configuration, options) => {
// react to the event
},
});
internalConvexStripe({
// ...
webhook: {
handlers: [myHandler],
},
});Redirect handlers let you run custom logic when a user is redirected back from Stripe (after a checkout, portal session, or account link flow). Use defineRedirectHandler to create one:
import { defineRedirectHandler } from "@raideno/convex-stripe/server";
const myRedirectHandler = defineRedirectHandler({
origins: ["subscribe-success", "pay-success"],
handle: async (origin, context, data, configuration, options) => {
// run custom logic after a successful payment or subscription
},
});
internalConvexStripe({
// ...
redirect: {
ttlMs: 15 * 60 * 1000, // default: 15 minutes
handlers: [myRedirectHandler],
},
});The available redirect origins are:
| Origin | Trigger |
|---|---|
subscribe-success |
User completed a subscription checkout. |
subscribe-cancel |
User cancelled a subscription checkout. |
pay-success |
User completed a one time payment checkout. |
pay-cancel |
User cancelled a one time payment checkout. |
portal-return |
User returned from the billing portal. |
create-account-link-return |
Connected account completed onboarding. |
create-account-link-refresh |
Connected account link expired and needs refresh. |
The library also exports a buildSignedReturnUrl utility that you can use to manually build signed redirect URLs. This is the same function used internally by stripe.subscribe, stripe.pay, stripe.portal, and stripe.accounts.link to generate their success_url, cancel_url, and return_url values.
Each signed URL points to GET /stripe/return/<origin> on your Convex backend, carries an HMAC-SHA256 signature derived from your account_webhook_secret, and expires after the configured redirect.ttlMs (default: 15 minutes). When the user hits the URL, the library verifies the signature, checks expiry, runs any matching redirect handler, and then issues a 302 redirect to the targetUrl you specified. If verification fails or the link has expired, the user is redirected to your failureUrl instead (if provided).
import { buildSignedReturnUrl } from "@raideno/convex-stripe/server";
const url = await buildSignedReturnUrl({
configuration, // your InternalConfiguration object
origin: "pay-success", // one of the redirect origins listed above
targetUrl: "https://example.com/payments/success",
failureUrl: "https://example.com/payments/error", // optional
data: {
entityId: "user_123",
referenceId: "order_456",
// data fields depend on the origin
},
});You typically do not need to call this function directly, as the built-in actions already handle URL signing for you. It is useful when building custom checkout or redirect flows outside of the provided actions.
The second argument to internalConvexStripe is an optional options object.
const { stripe, store, sync } = internalConvexStripe(configuration, {
debug: true, // enable debug logging
base: "stripe", // base path for HTTP routes (default: "stripe")
store: "store", // name of the store mutation export (default: "store")
});If you want to build a marketplace or platform with Stripe Connect, follow these additional steps after completing the Usage setup.
Go to the Stripe Connect settings and enable Connect for your account.
Run the sync action from your Convex dashboard with the following arguments:
{
"tables": true,
"webhooks": {
"connect": true
}
}This creates a Connect webhook endpoint on your Stripe account. The webhook secret will be printed in the Convex function logs.
Set the Connect webhook secret as an environment variable:
npx convex env set STRIPE_CONNECT_WEBHOOK_SECRET "<secret>"Then update your configuration to include it:
// convex/stripe.ts
import { internalConvexStripe } from "@raideno/convex-stripe/server";
import schema from "./schema";
export const { stripe, store, sync } = internalConvexStripe({
schema: schema,
stripe: {
secret_key: process.env.STRIPE_SECRET_KEY!,
account_webhook_secret: process.env.STRIPE_ACCOUNT_WEBHOOK_SECRET!,
connect_webhook_secret: process.env.STRIPE_CONNECT_WEBHOOK_SECRET!,
}
});Create a connected account for each seller and generate an onboarding link:
// convex/connect.ts
import { v } from "convex/values";
import { action } from "./_generated/server";
import { stripe } from "./stripe";
export const createSellerAccount = action({
args: { entityId: v.string(), email: v.string() },
handler: async (context, args) => {
// create the connected account
const account = await stripe.accounts.create(context, {
entityId: args.entityId,
email: args.email,
controller: {
fees: { payer: "application" },
losses: { payments: "application" },
stripe_dashboard: { type: "express" },
},
});
// generate the onboarding link
const link = await stripe.accounts.link(context, {
account: account.accountId,
refresh_url: "http://localhost:3000/connect/refresh",
return_url: "http://localhost:3000/connect/return",
type: "account_onboarding",
collection_options: { fields: "eventually_due" },
});
return link.url;
},
});Create products and prices on connected accounts by passing stripeAccount in the Stripe request options:
const product = await stripe.client.products.create(
{ name: "Widget", default_price_data: { currency: "usd", unit_amount: 1000 } },
{ stripeAccount: account.accountId },
);Payouts to connected accounts are handled by Stripe automatically based on your Connect payout schedule. You can also create manual transfers using the stripe.client:
const transfer = await stripe.client.transfers.create({
amount: 1000,
currency: "usd",
destination: account.accountId,
});Creates or retrieves a Stripe customer for a given entity. If the entity already has a Stripe customer associated with it, the existing customer is returned instead of creating a duplicate.
This should be called whenever a new entity is created in your app. See Stripe Customers for integration examples.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
entityId |
string |
Yes | Your app's internal identifier for the entity being billed. |
email |
string |
No | Email address for the Stripe customer. Recommended. |
metadata |
object |
No | Additional metadata to attach to the Stripe customer. |
All other parameters from Stripe.CustomerCreateParams are also accepted.
Returns: The Stripe customer document from your Convex database.
const customer = await stripe.customers.create(context, {
entityId: args.entityId,
email: args.email,
});Creates a Stripe Checkout session in subscription mode. Calls stripe.checkout.sessions.create under the hood.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
entityId |
string |
Yes | Your app's internal identifier for the entity subscribing. |
priceId |
string |
Yes | The Stripe Price ID for the subscription. |
mode |
"subscription" |
Yes | Must be "subscription". |
success_url |
string |
Yes | URL to redirect to after a successful checkout. |
cancel_url |
string |
Yes | URL to redirect to if the user cancels. |
failure_url |
string |
No | URL to redirect to if the redirect signing fails. |
createStripeCustomerIfMissing |
boolean |
No | If true (default), creates a Stripe customer automatically if one does not exist for the entity. |
All other parameters from Stripe.Checkout.SessionCreateParams (except customer, ui_mode, mode, line_items, client_reference_id, success_url, cancel_url) are also accepted.
An optional third argument accepts Stripe.RequestOptions (e.g. stripeAccount for Connect).
Returns: A Stripe.Checkout.Session. Use the url property to redirect the user.
export const createSubscription = action({
args: { entityId: v.string(), priceId: v.string() },
handler: async (context, args) => {
const response = await stripe.subscribe(context, {
entityId: args.entityId,
priceId: args.priceId,
mode: "subscription",
success_url: "https://example.com/payments/success",
cancel_url: "https://example.com/payments/cancel",
});
return response.url;
},
});Creates a Stripe Checkout session in payment mode for one time payments. Calls stripe.checkout.sessions.create under the hood.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
entityId |
string |
Yes | Your app's internal identifier for the entity making the payment. |
referenceId |
string |
Yes | Your app's reference ID for this payment (e.g. an order ID). Stored as the client_reference_id on the checkout session. |
mode |
"payment" |
Yes | Must be "payment". |
line_items |
array |
Yes | The line items for the checkout session (price, quantity, etc.). |
success_url |
string |
Yes | URL to redirect to after a successful payment. |
cancel_url |
string |
Yes | URL to redirect to if the user cancels. |
failure_url |
string |
No | URL to redirect to if the redirect signing fails. |
createStripeCustomerIfMissing |
boolean |
No | If true (default), creates a Stripe customer automatically if one does not exist for the entity. |
All other parameters from Stripe.Checkout.SessionCreateParams (except customer, ui_mode, mode, client_reference_id, success_url, cancel_url) are also accepted.
An optional third argument accepts Stripe.RequestOptions.
Returns: A Stripe.Checkout.Session. Use the url property to redirect the user.
export const createPayment = action({
args: { entityId: v.string(), orderId: v.string(), priceId: v.string() },
handler: async (context, args) => {
const response = await stripe.pay(context, {
referenceId: args.orderId,
entityId: args.entityId,
mode: "payment",
line_items: [{ price: args.priceId, quantity: 1 }],
success_url: `${process.env.SITE_URL}/payments/success`,
cancel_url: `${process.env.SITE_URL}/payments/cancel`,
});
return response.url;
},
});Opens a Stripe Billing Portal session for an existing customer. Allows users to manage their subscriptions, invoices, and payment methods. Calls stripe.billingPortal.sessions.create under the hood.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
entityId |
string |
Yes | Your app's internal identifier for the entity. |
return_url |
string |
Yes | URL to redirect to when the user leaves the portal. |
failure_url |
string |
No | URL to redirect to if the redirect signing fails. |
createStripeCustomerIfMissing |
boolean |
No | If true (default), creates a Stripe customer automatically if one does not exist for the entity. |
All other parameters from Stripe.BillingPortal.SessionCreateParams (except customer and return_url) are also accepted.
An optional third argument accepts Stripe.RequestOptions.
Returns: A Stripe.BillingPortal.Session. Use the url property to redirect the user.
export const openPortal = action({
args: { entityId: v.string() },
handler: async (context, args) => {
const response = await stripe.portal(context, {
entityId: args.entityId,
return_url: "https://example.com/account",
});
return response.url;
},
});Creates a new Stripe Connect account and stores it in your Convex database. If an account already exists for the given entity, the existing account is returned.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
entityId |
string |
Yes | Your app's internal identifier for the seller/platform entity. |
All other parameters from Stripe.AccountCreateParams (except type) are also accepted.
An optional third argument accepts Stripe.RequestOptions.
Returns: The Stripe account document from your Convex database.
const account = await stripe.accounts.create(context, {
entityId: args.entityId,
email: args.email,
controller: {
fees: { payer: "application" },
losses: { payments: "application" },
stripe_dashboard: { type: "express" },
},
});Creates a Stripe Connect Account Link for onboarding. Redirects the connected account holder to Stripe's hosted onboarding flow.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
account |
string |
Yes | The Stripe Account ID to onboard (e.g. acct_...). |
refresh_url |
string |
Yes | URL to redirect to if the link expires. |
return_url |
string |
Yes | URL to redirect to after onboarding is complete. |
failure_url |
string |
No | URL to redirect to if the redirect signing fails. |
All other parameters from Stripe.AccountLinkCreateParams (except refresh_url and return_url) are also accepted.
An optional third argument accepts Stripe.RequestOptions.
Returns: A Stripe.AccountLink. Use the url property to redirect the user.
const link = await stripe.accounts.link(context, {
account: account.accountId,
refresh_url: "https://example.com/connect/refresh",
return_url: "https://example.com/connect/return",
type: "account_onboarding",
collection_options: { fields: "eventually_due" },
});Registers the Stripe webhook and redirect routes on your Convex HTTP router. Call this inside your convex/http.ts file.
Registers two routes:
POST /stripe/webhookreceives and verifies Stripe webhook events.GET /stripe/return/*handles post-checkout and post-portal redirect flows.
const http = httpRouter();
stripe.addHttpRoutes(http);
export default http;A pre-configured Stripe SDK client using your secret_key and API version. Use this for any Stripe API call not covered by the library's built-in functions.
const product = await stripe.client.products.create({
name: "New Product",
default_price_data: { currency: "usd", unit_amount: 999 },
});An internal action that synchronizes Stripe resources with your Convex database.
This action is typically called manually from the Convex dashboard, or set up to be called automatically in your CI/CD pipeline on each deployment.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
tables |
boolean | { withConnect: boolean } |
Yes | If true, syncs all existing Stripe resources to Convex tables. If an object with withConnect: true, also syncs resources from linked connected accounts. |
webhooks |
{ account?: boolean, connect?: boolean } |
No | If account is true, creates or updates the account webhook endpoint. If connect is true, creates or updates the Connect webhook endpoint. The webhook secret is printed to the function logs when a new endpoint is created. |
portal |
boolean |
No | If true, creates the default billing portal configuration if it does not already exist. |
catalog |
boolean |
No | If true, creates or updates the products and prices defined in your sync.catalog configuration. |
An internal mutation that persists Stripe objects into your Convex database. This is called automatically from within the webhook handler and is not meant for direct use. It must be exported from the same file as your internalConvexStripe call.
Returns a set of pre-built, authorization-aware Convex functions that cover the most common Stripe operations. These are ready to export and call from your frontend directly — no boilerplate required.
Each returned function invokes your authenticateAndAuthorize callback to resolve the caller's identity before delegating to the underlying Stripe implementation. When a caller omits entityId, it means they want to act on themselves — your callback is responsible for deriving their identity from the Convex context.
stripe.helpers({ authenticateAndAuthorize: async ({ context, operation, entityId }) => { // Return [isAuthorized, resolvedEntityId | null] } })
**`authenticateAndAuthorize` parameters (passed as a single object):**
| Parameter | Type | Description |
| :---------- | :--------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `context` | `GenericActionCtx<any>` or `GenericQueryCtx<any>` | The Convex context, **narrowed by `operation`**: action ops (`createCustomer`, `subscribe`, `pay`, `portal`) receive `GenericActionCtx`; query ops (`products`, `subscription`, `customer`) receive `GenericQueryCtx`. |
| `operation` | `"createCustomer" \| "subscribe" \| "pay" \| "products" \| "subscription" \| "customer" \| "portal"` | The operation being performed. |
| `entityId` | `string \| undefined` | The entity ID passed by the caller, or `undefined` if acting on themselves. |
**Returns:** `Promise<[boolean, string | null]>` — `[isAuthorized, entityId]`.
**Returned functions:**
| Name | Kind | Description |
| :--------------- | :--------------- | :------------------------------------------- |
| `createCustomer` | `internalAction` | Create a Stripe customer for a given entity. |
| `products` | `query` | List all synced products with their prices. |
| `subscription` | `query` | Get the entity's active subscription. |
| `customer` | `query` | Get the entity's Stripe customer record. |
## Pre-built Helper Functions
`stripe.helpers()` is the fastest way to add Stripe to your app. Instead of hand-writing each Convex function, call `stripe.helpers()` once and export the returned functions:
```ts
// convex/stripe.ts
import { getAuthUserId } from "@convex-dev/auth/server";
import { internalConvexStripe } from "@raideno/convex-stripe/server";
import type { HelperAuthCallback } from "@raideno/convex-stripe/server";
import schema from "./schema";
export const { stripe, store, sync } = internalConvexStripe({
schema,
stripe: {
secret_key: process.env.STRIPE_SECRET_KEY!,
account_webhook_secret: process.env.STRIPE_ACCOUNT_WEBHOOK_SECRET!,
},
});
// A single callback that authenticates every helper function.
// `context` is automatically typed as GenericActionCtx or GenericQueryCtx
// depending on which `operation` is being executed.
const authenticateAndAuthorize: HelperAuthCallback = async ({
context, // GenericActionCtx<any> or GenericQueryCtx<any>
operation,
entityId,
}) => {
// For product listings there is no specific entity — allow everyone.
if (operation === "products") return [true, null];
const userId = await getAuthUserId(context);
if (!userId) return [false, null];
// If the caller passed an explicit entityId, use it; otherwise they act on themselves.
return [true, entityId ?? userId];
};
export const {
createCustomer,
products,
subscription,
customer,
} = stripe.helpers({
authenticateAndAuthorize
});
Then register createCustomer with your auth callbacks (the same way as the manual approach), and call the rest from your frontend:
// frontend
const items = await convex.query(api.stripe.products, {});
const sub = await convex.query(api.stripe.subscription, {});
const me = await convex.query(api.stripe.customer, {});
// Return URLs are handled by helper config, so frontend calls stay clean.
const { url } = await convex.action(api.stripe.subscribe, {
priceId: "price_xxx",
});
const { url } = await convex.action(api.stripe.pay, {
referenceId: "order_123",
line_items: [{ price: "price_yyy", quantity: 1 }],
});
const { url } = await convex.action(api.stripe.portal, {});An internal Convex action that creates a Stripe customer for the given entityId. Designed to be called from auth lifecycle callbacks (e.g. afterUserCreatedOrUpdated), not from the frontend directly.
Arguments:
| Parameter | Type | Required | Description |
|---|---|---|---|
entityId |
string |
Yes | Your app's internal identifier for the entity. |
email |
string |
No | Email address to set on the Stripe customer record. |
Returns: The Stripe customer document from your Convex database.
A public Convex query that returns all synced Stripe products with their associated prices nested inside.
Arguments: none
Returns: An array of product documents from stripeProducts, each with a prices field containing all related entries from stripePrices.
A public Convex query that returns the active Stripe subscription for the authenticated entity, or null if they have no subscription.
Arguments:
| Parameter | Type | Required | Description |
|---|---|---|---|
entityId |
string |
No | Override the entity (defaults to the caller). |
Returns: The subscription document from stripeSubscriptions, or null.
A public Convex query that returns the Stripe customer record for the authenticated entity, or null if they have no customer yet.
Arguments:
| Parameter | Type | Required | Description |
|---|---|---|---|
entityId |
string |
No | Override the entity (defaults to the caller). |
Returns: The customer document from stripeCustomers, or null.
The library automatically syncs the following 24 Stripe resource types into your Convex database:
| Table | ID Field | Description |
|---|---|---|
stripeAccounts |
accountId |
Connected accounts |
stripeProducts |
productId |
Products |
stripePrices |
priceId |
Prices |
stripeCustomers |
customerId |
Customers |
stripeSubscriptions |
subscriptionId |
Subscriptions |
stripeCoupons |
couponId |
Coupons |
stripePromotionCodes |
promotionCodeId |
Promotion codes |
stripePayouts |
payoutId |
Payouts |
stripeRefunds |
refundId |
Refunds |
stripePaymentIntents |
paymentIntentId |
Payment intents |
stripeCheckoutSessions |
checkoutSessionId |
Checkout sessions |
stripeInvoices |
invoiceId |
Invoices |
stripeReviews |
reviewId |
Reviews |
stripePlans |
planId |
Plans |
stripeDisputes |
disputeId |
Disputes |
stripeEarlyFraudWarnings |
earlyFraudWarningId |
Early fraud warnings |
stripeTaxIds |
taxIdId |
Tax IDs |
stripeSetupIntents |
setupIntentId |
Setup intents |
stripeCreditNotes |
creditNoteId |
Credit notes |
stripeCharges |
chargeId |
Charges |
stripePaymentMethods |
paymentMethodId |
Payment methods |
stripeSubscriptionSchedules |
subscriptionScheduleId |
Subscription schedules |
stripeMandates |
mandateId |
Mandates |
stripeBillingPortalConfigurations |
billingPortalConfigurationId |
Billing portal configurations |
stripeTransfers |
transferId |
Transfers |
stripeCapabilities |
capabilityId |
Capabilities |
Each table stores the full Stripe object in a stripe field and includes a lastSyncedAt timestamp. All tables have a byStripeId index on their ID field. Tables with an accountId field also have a byAccountId index for filtering by connected account.
For the full schema of each table, see Tables Reference.
The library handles a large number of Stripe webhook events to keep your local data in sync. Below is a summary by resource type. For the full list, see Events Reference.
| Resource | Events |
|---|---|
| Subscriptions | customer.subscription.created, updated, deleted, paused, resumed, etc. |
| Checkout Sessions | checkout.session.completed, expired, async_payment_succeeded, async_payment_failed |
| Customers | customer.created, updated, deleted |
| Invoices | invoice.created, paid, payment_failed, finalized, voided, etc. |
| Payment Intents | payment_intent.created, succeeded, canceled, payment_failed, etc. |
| Products | product.created, updated, deleted |
| Prices | price.created, updated, deleted |
| Charges | charge.captured, succeeded, failed, refunded, etc. |
| Refunds | refund.created, updated, failed |
| Payouts | payout.created, paid, failed, canceled, etc. |
| Disputes | charge.dispute.created, updated, closed, etc. |
| Payment Methods | payment_method.attached, detached, updated, etc. |
| Setup Intents | setup_intent.created, succeeded, canceled, setup_failed, etc. |
| Coupons | coupon.created, updated, deleted |
| Promotion Codes | promotion_code.created, updated |
| Credit Notes | credit_note.created, updated, voided |
| Reviews | review.opened, closed |
| Plans | plan.created, updated, deleted |
| Tax IDs | customer.tax_id.created, updated, deleted |
| Early Fraud Warnings | radar.early_fraud_warning.created, updated |
| Subscription Schedules | subscription_schedule.created, updated, canceled, completed, etc. |
- Always create a Stripe customer (
stripe.customers.create) the moment a new entity is created in your app. This ensures every user or organization has a Stripe customer ready for billing. - Use
metadataormarketing_featureson Stripe products to store feature flags or usage limits. You can then query the syncedstripeProductstable to check entitlements. - Run the
syncaction when you first configure the library, and after each deployment, to ensure your local database is up to date with Stripe. - Never expose internal actions directly to clients. Always wrap them in public actions with proper authentication and authorization checks.
- Keep your webhook secrets secure. Never commit them to source control. Always use Convex environment variables.
Clone the repository:
git clone git@github.com:raideno/convex-stripe.git
cd convex-stripeInstall the dependencies:
npm installStart the development server:
# automatically rebuild lib on changes
npm run dev --workspace @raideno/convex-stripe
# run the demo app
npm run dev --workspace demoAll contributions are welcome. Please open an issue or a pull request.