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:
| Mode | Description |
|---|---|
| disabled | Subscriptions are off. The /subscription page redirects to /. This is the default. |
| organization | One subscription per organization. Any admin or member can subscribe, and the plan covers all members. |
| member | Each 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
- Go to the Stripe Dashboard.
- Copy your Secret key and Publishable key.
- 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.
- Go to Products in Stripe.
- Create a product for each plan (e.g., "Starter", "Pro", "Enterprise").
- Add a recurring price to each product (monthly, yearly, etc.).
- 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/webhookThe CLI prints a webhook signing secret (whsec_...). Add it to your .env:
STRIPE_WEBHOOK_SECRET=whsec_...Production
- Go to Webhooks in Stripe.
- Add an endpoint pointing to
https://your-backend-url/api/subscription/webhook. - Select these events:
checkout.session.completedcustomer.subscription.updatedcustomer.subscription.deletedproduct.created,product.updated,product.deletedprice.created,price.updated,price.deleted
- Copy the signing secret to your production
STRIPE_WEBHOOK_SECRET.
How it works
Checkout flow
- User visits
/subscriptionand picks a plan. - Frontend calls
POST /api/subscription/checkoutwith the selectedstripePriceId. - Backend creates (or finds) a Stripe customer and generates a Checkout Session.
- User is redirected to Stripe's hosted checkout page.
- After payment, Stripe sends a
checkout.session.completedwebhook. - The webhook handler creates a
Subscriptionrecord 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:
| Status | Active | Description |
|---|---|---|
active | Yes | Subscription is paid and current |
trialing | Yes | In a free trial period |
incomplete | Yes | First payment pending (e.g., 3D Secure) |
past_due | Yes | Payment failed but retrying |
canceled | No | Subscription was canceled |
incomplete_expired | No | First payment was never completed |
unpaid | No | All payment retries exhausted |
paused | No | Subscription 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
| Method | Path | Description |
|---|---|---|
GET | /api/subscription/plans | List all plans from Stripe (cached) |
POST | /api/subscription/checkout | Create a Stripe Checkout Session |
POST | /api/subscription/portal | Create a Stripe Billing Portal session |
POST | /api/subscription/webhook | Stripe 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:
| Field | Description |
|---|---|
id | UUID primary key |
organizationId | FK to Organization (set in both modes) |
memberId | FK to Member (set in member mode only) |
userId | FK to User (always set) |
mode | organization or member |
status | Current Stripe subscription status |
stripeCustomerId | Stripe customer ID |
stripeSubscriptionId | Stripe subscription ID (unique) |
stripePriceId | Stripe price ID for the current plan |
cancelAt | When the subscription will be canceled (if set) |
Environment variables
| Variable | Required | Default | Description |
|---|---|---|---|
SUBSCRIPTION_MODE | No | disabled | disabled, organization, or member |
STRIPE_SECRET_KEY | Yes* | — | Stripe API secret key |
STRIPE_PUBLISHABLE_KEY | Yes* | — | Stripe publishable key (exposed to frontend) |
STRIPE_WEBHOOK_SECRET | Yes* | — | Stripe webhook signing secret |
* Required when SUBSCRIPTION_MODE is not disabled.