Subscriptions and payments

Your project includes a complete subscription billing system powered by Stripe. It supports multiple subscription modes, automatic plan syncing from the Stripe Dashboard, checkout, the Stripe customer portal, and webhook-driven lifecycle management.

Subscription modes

The billing behavior is controlled by the SUBSCRIPTION_MODE environment variable:

ModeDescription
disabledSubscriptions are off. The /subscription page redirects to /. This is the default.
organizationOne subscription per organization. Any admin or member can subscribe, and the plan covers all members.
memberEach member subscribes individually. The subscription is tied to a specific user within their organization.
# packages/backend/.env
SUBSCRIPTION_MODE=organization  # or "member" or "disabled"

Setting up Stripe

1. Get your API keys

  1. Go to the Stripe Dashboard.
  2. Copy your Secret key and Publishable key.
  3. Add them to your backend .env:
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...

2. Create products and prices

Plans are managed entirely in the Stripe Dashboard — no configuration files or database seeding needed.

  1. Go to Products in Stripe.
  2. Create a product for each plan (e.g., "Starter", "Pro", "Enterprise").
  3. Add a recurring price to each product (monthly, yearly, etc.).
  4. Optionally add marketing features to the product — these appear as a checklist on the plan card.

The app fetches plans directly from the Stripe API and caches them in Redis for 5 minutes. When you update products or prices in Stripe, the cache is automatically invalidated via webhooks.

3. Set up webhooks

Webhooks are required for the subscription lifecycle to work (creating records, updating statuses, etc.).

Local development

Use the Stripe CLI to forward webhooks to your local server:

# Install the Stripe CLI, then:
stripe login
stripe listen --forward-to localhost:3011/api/subscription/webhook

The CLI prints a webhook signing secret (whsec_...). Add it to your .env:

STRIPE_WEBHOOK_SECRET=whsec_...

Production

  1. Go to Webhooks in Stripe.
  2. Add an endpoint pointing to https://your-backend-url/api/subscription/webhook.
  3. Select these events:
    • checkout.session.completed
    • customer.subscription.updated
    • customer.subscription.deleted
    • product.created, product.updated, product.deleted
    • price.created, price.updated, price.deleted
  4. Copy the signing secret to your production STRIPE_WEBHOOK_SECRET.

How it works

Checkout flow

  1. User visits /subscription and picks a plan.
  2. Frontend calls POST /api/subscription/checkout with the selected stripePriceId.
  3. Backend creates (or finds) a Stripe customer and generates a Checkout Session.
  4. User is redirected to Stripe's hosted checkout page.
  5. After payment, Stripe sends a checkout.session.completed webhook.
  6. The webhook handler creates a Subscription record in the database and sends a notification to organization admins.

Managing a subscription

Once subscribed, the plan card shows a Manage button instead of Subscribe. Clicking it opens the Stripe Customer Portal, where users can:

  • View invoices and payment history
  • Update their payment method
  • Switch plans (upgrade or downgrade)
  • Cancel the subscription

Only the user who originally created the subscription can access the portal.

Subscription lifecycle

The system tracks these statuses from Stripe:

StatusActiveDescription
activeYesSubscription is paid and current
trialingYesIn a free trial period
incompleteYesFirst payment pending (e.g., 3D Secure)
past_dueYesPayment failed but retrying
canceledNoSubscription was canceled
incomplete_expiredNoFirst payment was never completed
unpaidNoAll payment retries exhausted
pausedNoSubscription is paused

When a subscription is canceled with a future cancelAt date, the plan card shows a warning banner with the cancellation date.

Subscription page

The /subscription page displays all active plans from Stripe as cards. Each card shows:

  • Plan name and description
  • Price formatted with the correct currency and interval (e.g., "$19/month")
  • Marketing features as a checklist
  • A Subscribe button, or Manage / Current badge for the active plan

When multiple billing intervals exist (e.g., monthly and yearly), plans are grouped into tabs. Plans are sorted by price from lowest to highest.

Permissions

Both admin and member roles can manage subscriptions by default. The permission resource is subscription with actions read, create, and update.

The frontend route requires subscription: ['read']. Checkout and portal endpoints require subscription: ['create'].

To restrict subscription management to admins only, remove the subscription permissions from the member role in src/features/permissions.ts.

Cancellation on member disable

When an admin disables a member, all of that member's active subscriptions in the organization are automatically canceled on Stripe. This ensures disabled users don't continue to be billed.

API endpoints

MethodPathDescription
GET/api/subscription/plansList all plans from Stripe (cached)
POST/api/subscription/checkoutCreate a Stripe Checkout Session
POST/api/subscription/portalCreate a Stripe Billing Portal session
POST/api/subscription/webhookStripe webhook receiver

Plans are also included in the public config endpoint (GET /api/config/public) when subscriptions are enabled.

Database schema

The Subscription model stores:

FieldDescription
idUUID primary key
organizationIdFK to Organization (set in both modes)
memberIdFK to Member (set in member mode only)
userIdFK to User (always set)
modeorganization or member
statusCurrent Stripe subscription status
stripeCustomerIdStripe customer ID
stripeSubscriptionIdStripe subscription ID (unique)
stripePriceIdStripe price ID for the current plan
cancelAtWhen the subscription will be canceled (if set)

Environment variables

VariableRequiredDefaultDescription
SUBSCRIPTION_MODENodisableddisabled, organization, or member
STRIPE_SECRET_KEYYes*Stripe API secret key
STRIPE_PUBLISHABLE_KEYYes*Stripe publishable key (exposed to frontend)
STRIPE_WEBHOOK_SECRETYes*Stripe webhook signing secret

* Required when SUBSCRIPTION_MODE is not disabled.