TinyKit Pro Docs

Billing & Subscription Management

TinyKit Pro includes comprehensive Stripe integration for subscription billing, customer management, and payment processing.

TinyKit Pro includes comprehensive Stripe integration for subscription billing, customer management, and payment processing.

Billing Features

  • ๐Ÿ’ณ Subscription Plans: Multiple tiers with monthly/yearly billing
  • ๐Ÿช Customer Portal: Stripe-powered self-service billing management
  • ๐Ÿ“Š Usage Tracking: Monitor subscription usage and limits
  • ๐Ÿ”„ Smart Proration: Accurate proration for plan changes and upgrades
  • ๐Ÿ•’ Test Clock Support: Development environment with frozen time for testing
  • ๐Ÿ“ง Billing Notifications: Automated email alerts for billing events
  • ๐ŸŽซ Promotion Codes: Discount support with automatic application
  • โฐ Real-time Expiration: Intelligent subscription expiration with graceful access control
  • ๐Ÿงช Trial Period Tracking: Accurate trial start/end date tracking with proper end date display
  • ๐Ÿšซ Cancellation Management: Complete tracking of subscription cancellations with cancelAt and canceledAt timestamps
  • ๐Ÿ“… Effective End Dates: Smart display logic showing trial end dates vs billing period end dates
  • ๐ŸŒ Flexible Entity Types: Support for both personal and organization-level subscriptions with seamless switching

Subscription Plans

Available Plans

Personal Plans:

  • Free: Basic workspace access, up to 2 organization members
  • Solo: $9/month, unlimited personal workspaces, advanced features
  • Premium: $19/month, everything in Solo plus analytics and API access

Organization Plans:

  • Starter: $19/month or $190/year, up to 5 organization members
  • Professional: $29/month or $290/year, up to 20 organization members (recommended)
  • Enterprise: $39/month or $390/year, unlimited members, enterprise features
  • Lifetime: $500 one-time, everything in Enterprise with lifetime access

Plan Configuration

Plans are configured in the seed configuration file:

Location: /convex/seeds/stripeProductsHelper.ts

Subscription Type Selection

The system now provides a seamless subscription type selection flow:

  1. Type Selection: When signing up for a plan, users can choose:

    • Personal: Tied to their individual account
    • Organization: For team usage with shared billing
  2. Flow Integration:

    // Store selected plan with subscription type
    storeSelectedPlan({
      planName: "Professional",
      priceId: "price_pro_monthly",
      amount: 2900,
      billingInterval: "month",
      subscriptionType: "org", // "personal" | "org"
    });
  3. Organization Creation:

    • Personal subscriptions proceed directly to checkout
    • Organization subscriptions prompt for organization creation first
    • New organizations automatically become the billing entity

Landing Page Plan Display

The landing page pricing display is controlled by the defaultSubscriptionType setting in the database, configurable through the admin billing settings panel:

  • Personal: Shows only personal plans on the landing page
  • Organization: Shows only organization plans on the landing page
  • Both: Shows all plans (both personal and organization)

This is separate from the enableOrganizations feature setting (controlled via Admin > Site Settings > General), which controls UI availability. The database setting allows for marketing flexibility independent of feature availability.

Access: Admin Panel โ†’ Billing Settings โ†’ Landing Page Settings

interface ProductConfig {
  name: string; // Internal product name
  displayName: string; // User-facing product name
  description: string; // Product description
  active: boolean; // Availability status
  type: SubscriptionType;
  accessLevel: number; // Feature access level (higher = more features)
  displayOrder: number; // UI display ordering
  isRecommended?: boolean;
  features: string[]; // Feature list for marketing
  prices: ProductPrice[]; // Associated pricing options
}

Smart Proration System

Test Clock Integration

The billing system uses Stripe test clocks for accurate proration testing in development:

// Environment-aware proration calculation
const isProduction = !process.env.STRIPE_SECRET_KEY?.includes("sk_test_");

if (!isProduction && subscriptionWithTestClock.test_clock) {
  // Use test clock's frozen time for accurate testing
  const testClock = await stripe.testHelpers.testClocks.retrieve(
    subscriptionWithTestClock.test_clock,
  );
  prorationDate = testClock.frozen_time;
} else {
  // Use actual current time for production
  prorationDate = Math.floor(Date.now() / 1000);
}

Proration Benefits

  • Accurate Pricing: Prorations reflect actual elapsed billing periods
  • Development Testing: Test clock support for consistent testing
  • Fair Billing: Customers pay only for time used
  • Transparent Calculations: Detailed proration breakdown in checkout

Testing Proration

# Create test clock with frozen time
stripe test_clocks create --frozen-time 1640995200  # Jan 1, 2022

# Advance test clock to simulate time passage
stripe test_clocks advance clock_1234 --frozen-time 1641600000  # Jan 8, 2022

# Check proration calculations
# Should show ~$7.50 for 1 week elapsed instead of $10

Subscription Management

Subscription States

  • Active: Subscription is current and billing normally
  • Past Due: Payment failed, grace period active
  • Canceled: Subscription ended, access restricted
  • Trialing: In trial period, full access without payment
  • Incomplete: Payment method needs authentication

Subscription Expiration Handling

The system includes intelligent subscription expiration handling that provides real-time access control:

Real-time Expiration Checks

  • Immediate Protection: Access is checked in real-time, no waiting for webhooks or scheduled jobs
  • Graceful Cancellation: Users retain access until their paid period expires
  • Automatic Revocation: Access is automatically denied after currentPeriodEnd date

Implementation Details

// Enhanced subscription activity check (uses Stripe component data)
export function isSubscriptionActive(subscription: {
  status: string;
  cancelAtPeriodEnd?: boolean;
  currentPeriodEnd?: number;
}): boolean {
  const isStatusActive =
    subscription.status === "active" || subscription.status === "trialing";

  // If status is not active, subscription is definitely not active
  if (!isStatusActive) {
    return false;
  }

  // If subscription is canceled at period end and has expired, it's not active
  const hasExpired = subscription.currentPeriodEnd
    ? subscription.currentPeriodEnd * 1000 < Date.now()
    : false;

  if (subscription.cancelAtPeriodEnd && hasExpired) {
    return false;
  }

  return true;
}

Note: Subscription data (status, cancelAtPeriodEnd, currentPeriodEnd) is fetched from the @convex-dev/stripe component, not stored in local tables.

Access Control Flow

  1. Before Cancellation: status: "active" + cancelAtPeriodEnd: false โ†’ โœ… Access granted
  2. After Cancellation (Grace Period): status: "active" + cancelAtPeriodEnd: true + currentPeriodEnd > Date.now() โ†’ โœ… Access granted
  3. After Period End: status: "active" + cancelAtPeriodEnd: true + currentPeriodEnd < Date.now() โ†’ โŒ Access denied

Benefits

  • No Infrastructure Changes: Uses existing database fields and logic
  • Backward Compatible: Existing code automatically benefits from enhanced checks
  • Consistent Behavior: Both team and personal subscriptions use the same logic
  • Defense in Depth: Works alongside webhook processing for robustness

Plan Changes

// Upgrade/downgrade with smart button labels
const changeSubscription = useMutation(api.billing.actions.changeSubscription);

await changeSubscription({
  teamId: team._id,
  newPriceId: "price_professional_monthly",
  prorationBehavior: "create_prorations",
});

Dynamic Button Labels

The system automatically determines appropriate action labels:

  • "Upgrade": Moving to more expensive plan
  • "Downgrade": Moving to less expensive plan
  • "Purchase": Starting new subscription
  • "Change Billing": Switching between monthly/yearly

Enhanced Subscription Tracking

Trial Period Management

The system now includes comprehensive trial period tracking to ensure accurate date display and user experience:

Data Source:

Trial and cancellation data is stored in the @convex-dev/stripe component and fetched when needed:

// Fields available from Stripe component subscription data:
// - trial_start: Trial start timestamp
// - trial_end: Trial end timestamp
// - cancel_at: Scheduled cancellation timestamp
// - canceled_at: Actual cancellation timestamp
// - current_period_end: Billing period end timestamp

Key Features:

  • Accurate Trial Dates: Displays actual 3-day trial period instead of 30-day billing period
  • Smart End Date Logic: Shows trial end date during trial, billing period end date after
  • Cancellation Tracking: Tracks both scheduled and actual cancellation timestamps
  • Real-time Status: Dynamically updates subscription status display

Subscription Status Display Logic

The billing information card now uses intelligent logic to show the most relevant dates:

// Priority order for displaying subscription end dates:
1. Trial period (if status === "trialing") โ†’ show trialEnd
2. Scheduled cancellation โ†’ show cancelAt
3. Active subscription โ†’ show currentPeriodEnd
4. Past due โ†’ show currentPeriodEnd with warning

Example Display States:

// Trial subscription (3-day trial)
"Your Starter subscription trial ends on October 26, 2025";

// Active subscription with scheduled cancellation
"Your Starter subscription is scheduled to end on November 15, 2025";

// Regular active subscription
"Your Starter subscription renews on November 15, 2025";

// Past due subscription
"Your Starter subscription was due on November 15, 2025";

Webhook Synchronization

Enhanced webhook processing ensures accurate trial and cancellation data:

// Stripe webhook events now capture:
- customer.subscription.created โ†’ trialStart, trialEnd
- customer.subscription.updated โ†’ cancelAt updates
- customer.subscription.deleted โ†’ canceledAt timestamp

Benefits:

  • User Clarity: Users see exact trial duration instead of confusing billing periods
  • Accurate Status: Real-time subscription status updates from Stripe
  • Better UX: Clear messaging about when subscriptions end vs renew
  • Cancellation Tracking: Complete audit trail of subscription changes

Promotion Code Management

TinyKit Pro includes a comprehensive promotion code system integrated with Stripe:

Promotion Code Features

  • Flexible Discounts: Support for both percentage and fixed amount discounts
  • Usage Limits: Optional maximum usage count for codes
  • Expiration Control: Optional expiration dates for time-limited promotions
  • Stripe Integration: Automatic syncing with Stripe coupon system
  • Usage Tracking: Real-time tracking of promotion code usage

Creating Promotion Codes

const createPromoCode = useMutation(api.billing.private.createPromoCode);

await createPromoCode({
  code: "LAUNCH2024",
  type: "percentage",
  amount: 20, // 20% off
  maxUses: 100,
  expiresAt: new Date("2025-01-01").getTime(),
});

Applying Promotion Codes

const applyPromoCode = useMutation(api.billing.private.applyPromoCode);

await applyPromoCode({
  code: "LAUNCH2024",
  subscriptionId: subscription._id,
});

Managing Promotion Codes

Admins can manage promotion codes through the admin interface:

  1. Create new promotion codes with:

    • Code identifier
    • Discount type (percentage/fixed)
    • Discount amount
    • Usage limits
    • Expiration dates
  2. Monitor usage statistics:

    • Total uses
    • Remaining uses
    • Total discount amount
    • Active/inactive status
  3. Administrative actions:

    • Deactivate codes
    • Adjust usage limits
    • Modify expiration dates

Implementation Details

// Promotion code schema
const promoCodes = defineTable({
  code: v.string(),
  type: v.union(v.literal("percentage"), v.literal("fixed")),
  amount: v.number(),
  maxUses: v.optional(v.number()),
  useCount: v.number(),
  expiresAt: v.optional(v.number()),
  active: v.boolean(),
  stripeCouponId: v.string(),
}).index("by_code", ["code"]);

// Check promotion code validity
export const validatePromoCode = mutation({
  args: { code: v.string() },
  handler: async (ctx, args) => {
    const promoCode = await ctx.db
      .query("promoCodes")
      .withIndex("by_code", (q) => q.eq("code", args.code))
      .first();

    if (!promoCode) throw new ConvexError("Invalid promotion code");
    if (!promoCode.active) throw new ConvexError("Promotion code is inactive");
    if (promoCode.expiresAt && promoCode.expiresAt < Date.now()) {
      throw new ConvexError("Promotion code has expired");
    }
    if (promoCode.maxUses && promoCode.useCount >= promoCode.maxUses) {
      throw new ConvexError("Promotion code has reached maximum uses");
    }

    return promoCode;
  },
});

Stripe Integration

The system automatically handles:

  1. Creating corresponding Stripe coupons
  2. Applying coupons during checkout
  3. Tracking usage in both systems
  4. Handling proration with discounts
  5. Retrieving promotion codes with embedded coupon data for performance
// Create Stripe coupon with promotion code
const stripeCoupon = await stripe.coupons.create({
  name: args.code,
  percent_off: args.type === "percentage" ? args.amount : undefined,
  amount_off: args.type === "fixed" ? args.amount : undefined,
  currency: args.type === "fixed" ? "usd" : undefined,
  max_redemptions: args.maxUses,
  redeem_by: args.expiresAt ? Math.floor(args.expiresAt / 1000) : undefined,
});

// Retrieve promotion code with expanded coupon data
const promotionCode = await stripe.promotionCodes.retrieve(promoCodeId, {
  expand: ["coupon"], // Includes full coupon details in response
});

Performance Optimization: The promotion code retrieval system now uses Stripe's expand parameter to include coupon details directly in the API response, eliminating the need for additional API calls and improving response times.

Automatic Webhook Synchronization

The billing system automatically syncs promotion codes when Stripe coupon webhooks fire:

How It Works:

  1. When a coupon is created or updated in Stripe, the coupon.created or coupon.updated webhook fires
  2. The system calls Stripe's API to fetch ALL promotion codes that reference this coupon
  3. Each promotion code (with embedded coupon data) is synced to the database using fresh data from Stripe
  4. Admins can immediately view and manage these codes in /admin/promotions

Benefits:

  • Always Fresh Data: Uses current Stripe API data instead of potentially stale webhook payloads
  • Automatic Discovery: Promotion codes created in Stripe Dashboard are automatically synced
  • No Manual Sync Required: Webhook-driven synchronization happens automatically
  • Coupon Deletion Handling: When coupons are deleted, all related promotion codes are marked inactive

Implementation:

// Webhook handler fetches promotion codes from Stripe API
export const handleCouponEvent = internalAction({
  handler: async ({ runMutation }, { event }) => {
    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {});
    const coupon = event.data.object as Stripe.Coupon;

    // Fetch ALL promotion codes for this coupon
    const promotionCodes = await stripe.promotionCodes.list({
      coupon: coupon.id,
      limit: 100,
      expand: ["data.coupon"], // Include full coupon data
    });

    // Sync each code to database
    for (const promotionCode of promotionCodes.data) {
      await runMutation(
        internal.billing.internal.mutations.upsertPromotionCode,
        {
          // ... promotion code data with embedded coupon details
        },
      );
    }
  },
});

Use Cases:

  • Create coupons in Stripe Dashboard โ†’ Automatically sync promotion codes
  • Update coupon discount amounts โ†’ All promotion codes reflect new values
  • Delete coupons โ†’ Related promotion codes marked inactive
  • Bulk import promotion codes in Stripe โ†’ Automatically appear in admin panel

Customer Portal Integration

Portal Features

Stripe Customer Portal provides self-service capabilities:

  • Payment Methods: Add, remove, and update payment methods
  • Billing History: View invoices and payment history
  • Plan Management: Upgrade, downgrade, or cancel subscriptions
  • Invoice Downloads: PDF invoice downloads

Portal Configuration

Important: Customer Portal must be configured in Stripe Dashboard:

  1. Go to Stripe Customer Portal Settings
  2. Click "Activate test link" or "Configure portal"
  3. Configure features: subscriptions, payment methods, billing history
  4. Add business information and support email
  5. Configure cancellation and plan switching options
  6. Click "Save" to activate

Portal Access

// Generate customer portal session
const createPortalSession = useMutation(
  api.billing.actions.createCustomerPortalSession,
);

const handleManageBilling = async () => {
  const { url } = await createPortalSession({
    teamId: team._id,
    returnUrl: window.location.href,
  });
  window.location.href = url;
};

Webhook Integration

Webhook Events

The system handles key Stripe webhook events:

  • customer.subscription.created: New subscription activation
  • customer.subscription.updated: Plan changes and modifications
  • customer.subscription.deleted: Cancellation handling
  • invoice.payment_succeeded: Successful payment processing
  • invoice.payment_failed: Failed payment notifications

Webhook Endpoint

Location: /convex/http.ts - handles /stripe endpoint

// Webhook signature verification
const sig = request.headers.get("stripe-signature");
const event = stripe.webhooks.constructEvent(body, sig!, webhookSecret);

// Event processing
switch (event.type) {
  case "customer.subscription.created":
    await handleSubscriptionCreated(event.data.object);
    break;
  case "customer.subscription.updated":
    await handleSubscriptionUpdated(event.data.object);
    break;
  // ... handle other events
}

Webhook Testing

# Forward webhooks to local development server
bun dev:stripe

# Test specific webhook events
stripe trigger customer.subscription.created
stripe trigger invoice.payment_succeeded

Product Sync System

Smart Sync Logic

The system intelligently manages product synchronization between seed configuration, Stripe, and database:

  1. Check Database: Count existing products in Convex
  2. Check Stripe: Count existing products in Stripe account
  3. Determine Action: Choose sync from Stripe or create from seed
  4. Execute Operation: Sync products and prices to database

Sync Scenarios

  • Products Exist: Show Stripe Dashboard link, disable sync
  • Sync from Stripe: Import existing Stripe products to database
  • Create from Seed: Create products in Stripe from configuration, then sync

Admin Interface

Product management available at /admin/products:

// Check system status and determine appropriate action
const status = useQuery(api.billing.actions.checkProductsStatus);

// Smart sync based on current state
const syncProducts = useMutation(
  api.billing.public.mutations.syncProductsFromStripe,
);

Billing Notifications

Automated Notifications

The system sends notifications for billing events:

  • Subscription Created: Welcome message with plan details
  • Subscription Updated: Plan change confirmations
  • Subscription Canceled: Cancellation confirmations with reactivation options
  • Payment Failed: Payment failure alerts with resolution steps
  • Invoice Paid: Payment confirmations and receipt delivery

Notification Integration

// Send billing notification
await createNotification(ctx, {
  userId: subscription.customerId,
  type: "subscription_created",
  title: "Welcome to Professional Plan!",
  message: "Your subscription is now active.",
  deliveryChannels: ["in-app", "email"],
  emailTemplate: "SubscriptionCreatedEmail",
});

Development & Testing

Test Mode Setup

  1. Use Test API Keys: Ensure STRIPE_SECRET_KEY contains sk_test_
  2. Test Clock: Create and use test clocks for time-based testing
  3. Test Cards: Use Stripe test card numbers for payments
  4. Webhook Testing: Use stripe listen for local webhook forwarding

Testing Checklist

  • Subscription creation and activation
  • Plan upgrades and downgrades with correct proration
  • Payment failure handling and retry logic
  • Customer portal access and functionality
  • Webhook delivery and processing
  • Billing notifications and email delivery
  • Organization subscription type selection
  • Organization creation during subscription flow

Debug Commands

# Monitor Stripe webhook events
stripe logs tail

# View Convex billing logs
npx convex logs --tail | grep "billing\|stripe\|subscription"

# Check subscription status in Stripe
stripe subscriptions list --limit 10

# Test proration calculations
stripe subscriptions update sub_1234 --proration-date $(date +%s)

Database Schema

TinyKit Pro uses the @convex-dev/stripe component for subscription, payment, and invoice storage. The application schema contains only minimal join tables that link the component data to our application entities.

Architecture Overview

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    @convex-dev/stripe Component                 โ”‚
โ”‚  (Handles subscriptions, invoices, payments, webhook events)    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ†• References via stripeSubscriptionId
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   Application Join Tables                        โ”‚
โ”‚  userSubscriptions โ”‚ orgSubscriptions โ”‚ products โ”‚ prices       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Tables Overview

TablePurposeKey Fields
userSubscriptionsJoin table: links users to Stripe subsuserId, stripeSubscriptionId, stripeCustomerId
orgSubscriptionsJoin table: links orgs to Stripe subsorgId, billingUserId, stripeSubscriptionId
productsSynced Stripe productsstripeProductId, name, type, accessLevel
pricesSynced Stripe pricesstripePriceId, amount, interval, trialDays
promotionCodesDiscount codes with embedded coupon datacode, percentOff, amountOff, duration

Note: Subscription details (status, period dates, trial info, cancellation) are stored in the Stripe component and fetched when needed - not duplicated locally.

User Subscriptions Join Table (Minimal)

// Links personal subscriptions from @convex-dev/stripe component to users
userSubscriptions: defineTable({
  userId: v.string(), // Better Auth user ID
  stripeSubscriptionId: v.string(), // Reference to component subscription
  stripeCustomerId: v.string(), // For portal/checkout creation
})
  .index("by_user", ["userId"])
  .index("by_stripe_subscription", ["stripeSubscriptionId"])
  .index("by_stripe_customer", ["stripeCustomerId"]);

Organization Subscriptions Join Table (Minimal)

// Links organization subscriptions from @convex-dev/stripe component to orgs
orgSubscriptions: defineTable({
  orgId: v.string(), // Better Auth org ID
  billingUserId: v.string(), // Owner who pays (Better Auth user ID)
  stripeSubscriptionId: v.string(), // Reference to component subscription
  stripeCustomerId: v.string(), // For portal/checkout creation
})
  .index("by_org", ["orgId"])
  .index("by_billing_user", ["billingUserId"])
  .index("by_stripe_subscription", ["stripeSubscriptionId"])
  .index("by_stripe_customer", ["stripeCustomerId"]);

Products Table

products: defineTable({
  stripeProductId: v.string(),
  name: v.string(),
  description: v.optional(v.string()),
  active: v.boolean(),
  defaultPriceId: v.optional(v.string()),
  features: v.optional(v.array(v.string())),
  displayOrder: v.optional(v.number()), // Display order (scoped per type)
  isRecommended: v.optional(v.boolean()),
  accessLevel: v.optional(v.number()), // 0=free, 1=pro, 2=enterprise
  type: SubscriptionTypeValidator, // "personal" | "org"
  icon: v.optional(v.string()),
  metadata: v.optional(v.any()),
  createdAt: v.number(),
  updatedAt: v.number(),
})
  .index("by_stripe_id", ["stripeProductId"])
  .index("by_active", ["active"])
  .index("by_type", ["type"])
  .index("by_type_displayOrder", ["type", "displayOrder"]);

Entity-Based Subscription Design

The schema uses separate join tables for personal and organization subscriptions:

  • Personal subscriptions: userSubscriptions table links userId โ†’ Stripe subscription
  • Organization subscriptions: orgSubscriptions table links orgId โ†’ Stripe subscription
  • Billing responsibility: orgSubscriptions.billingUserId tracks the org owner who pays

This design allows:

  • Users to have personal subscriptions independent of organizations
  • Organizations to have their own billing separate from members
  • Clear audit trail of who is financially responsible
  • Subscription data to stay in the Stripe component (single source of truth)

Related Schema Files:

  • convex/billing/schema.ts - Join tables and product/price definitions
  • convex/billing/validators.ts - Shared validators (BillingIntervalValidator, SubscriptionTypeValidator)

Troubleshooting

Common Issues

"No configuration provided" error in Customer Portal

  • Solution: Configure Customer Portal in Stripe Dashboard
  • Enable required features and business information

Webhook signature verification failed

  • Solution: Ensure STRIPE_WEBHOOKS_SECRET matches webhook endpoint secret
  • For local development, use secret from stripe listen output

Incorrect proration amounts

  • Solution: Verify test clock configuration and frozen time usage
  • Check that subscription is attached to test clock in development

Plan buttons showing wrong labels

  • Solution: Verify product pricing is synced correctly to database
  • Check that getCurrentPlanPrice() returns accurate pricing

Products not syncing

  • Solution: Check Stripe API credentials and webhook configuration
  • Verify admin permissions for product management functions

Subscription type modal not showing

  • Solution: Verify displayMode is set to "showcase" in PricingPlans
  • Check that user is not already authenticated

Organization creation failing during signup

  • Solution: Check logs for org creation errors
  • Verify userId and plan data are properly stored during flow

React key collision errors in admin subscription filters

  • Solution: Subscription plan filters now use unique product IDs instead of product names
  • This prevents collisions when products share names across types (e.g., "Free" exists for both personal and org)
  • Implementation: getAvailablePlans returns product._id as value, filter logic looks up product by ID
  • Benefits: More accurate filtering, stable keys even if product names change, no React warnings

โ† Previous: Organizations | Next: Notifications โ†’

On this page

Ship your startup faster. In minutes.

Get TinyKit Pro