ShipVeryFastShipVeryFast
Documentation

Billing model

This page explains how billing is wired and the one rule that keeps it correct. For the setup, see Payments & subscriptions.

The webhook is the source of truth

Checkout happens in Stripe, not in your app. After payment, Stripe sends webhook events, and the handler at app/api/webhooks/stripe/route.ts writes the result to your subscriptions table. Your app reads subscription state from your own database, never from the browser. This matters because the client can be tampered with, and a checkout can succeed seconds after the user closes the tab.

Checkout (Stripe)  ->  webhook event  ->  update subscriptions table  ->  app reads status

The lifecycle events

  • customer.subscription.updated, syncs the current status.
  • invoice.payment_succeeded, marks the subscription active.
  • invoice.payment_failed, marks it past due.

Self-service changes go through the Stripe billing portal, and the same webhook keeps your database in step.

Verify the signature

The webhook verifies the Stripe signature with STRIPE_WEBHOOK_SECRET before trusting any event. An unverified webhook is an open door, so never skip this check.

Gating access

Features are gated on the stored subscription status, not on a price ID in the client. Plans and their Stripe price IDs are resolved server-side in libs/pricingPlans.ts via getStripePriceId, so no real price IDs ship in the browser bundle.