npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@convex-dev/stripe

v0.1.3

Published

A stripe component for Convex.

Readme

@convex-dev/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-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

  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:
    • checkout.session.completed
    • customer.created
    • customer.updated
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
    • invoice.created
    • invoice.finalized
    • invoice.paid
    • invoice.payment_failed
    • payment_intent.succeeded
    • payment_intent.payment_failed
  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-dev/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-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
});

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

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