Skip to content

get-convex/stripe

Repository files navigation

@convex/stripe

A Convex component for integrating Stripe payments, subscriptions, and billing into your Convex application.

npm version

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/stripe

2. Add to Your Convex App

Create 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;

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

  1. Go to Stripe Dashboard → Developers → Webhooks
  2. Click "Add endpoint"
  3. Enter your webhook URL:
    https://<your-convex-deployment>.convex.site/stripe/webhook
    
    (Find your deployment name in the Convex dashboard - it's the part before .convex.cloud in your URL)
  4. Select these events:
    • customer.created
    • customer.updated
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
    • payment_intent.succeeded
    • payment_intent.payment_failed
    • invoice.created
    • invoice.finalized
    • invoice.paid
    • invoice.payment_failed
    • checkout.session.completed
  5. Click "Add endpoint"
  6. Copy the Signing secret and add it as STRIPE_WEBHOOK_SECRET in Convex

5. Register Webhook Routes

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;

6. Use the Component

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 },
    });
  },
});

API Reference

StripeSubscriptions Client

import { StripeSubscriptions } from "@convex/stripe";

const stripeClient = new StripeSubscriptions(components.stripe, {
  STRIPE_SECRET_KEY: "sk_...", // Optional, defaults to process.env.STRIPE_SECRET_KEY
});

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
});

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

Webhook Events

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

Custom Webhook Handlers

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;

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

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

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:

  1. Create a Clerk application at clerk.com
  2. Add VITE_CLERK_PUBLISHABLE_KEY to your .env.local
  3. 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:

  1. Set STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in Convex environment variables
  2. Configured the webhook endpoint in Stripe with the correct events
  3. Added invoice.created and invoice.finalized events (not just invoice.paid)

"Not authenticated" errors

Ensure your auth provider is configured:

  1. Create convex/auth.config.ts with your provider
  2. Run npx convex dev to push the config
  3. 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