Subscriptions & Billing
Subscriptions tie an account to a paid plan via your chosen payment gateway. Prime handles the full lifecycle — checkout, webhook processing, upgrades, and cancellations.
How it works
- User initiates checkout → the backend creates a session with the active payment gateway
- User completes payment on the gateway's hosted page
- The gateway fires a webhook to Prime → account is upgraded to the paid plan
- Subscription IDs and customer IDs are stored on the account for future billing events
Payment gateways
The active gateway is set by PAYMENT_GATEWAY in .env:
| Gateway | PAYMENT_GATEWAY value |
|---|---|
| Lemon Squeezy | lemonsqueezy |
| Stripe | stripe |
Switching gateways requires only changing PAYMENT_GATEWAY — no code changes needed — but you must also populate that gateway's price/variant IDs on each plan row and register its webhook endpoint. See Lemon Squeezy Setup for full configuration and for the current Stripe caveats.
API endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /payments | List payment history for the account |
GET | /invoices | List invoices (paginated with limit/offset) |
POST | /checkout-link | Generate a checkout URL for a plan (body: { plan_id, interval }) |
GET | /subscription/status | Get the account's current subscription |
POST | /subscription/cancel | Cancel the subscription (at period end; { immediately: true } for instant) |
POST | /subscription/resume | Undo a scheduled cancellation |
GET | /subscription/portal-url | Get the gateway's hosted billing portal URL |
POST | /account/upgrade | Change the active plan on an existing subscription |
POST | /admin/account/upgrade | Manual plan override, bypassing the gateway (master role) |
POST | /webhooks/lemonsqueezy | Lemon Squeezy payment webhook receiver |
POST | /webhooks/stripe | Stripe payment webhook receiver |
Plan upgrades
POST /account/upgrade (body: { planName }) changes the plan on an existing subscription through the active gateway's changePlan() method — a variant swap on Lemon Squeezy, a subscription-item price swap on Stripe. Proration is handled entirely by the gateway; the server never computes prorated prices.
Eligibility is checked first: a downgrade is rejected when the current team size exceeds the target plan's user limit, and the 400 response carries a violations array ({ resource, current, limit }) the client renders. The account's plan and billing cycle are then updated locally so the new limits apply immediately, and the gateway's subscription_updated webhook confirms the change.
Accounts without an active subscription must go through checkout instead.
Webhook security
Payment webhooks are verified using the gateway's signature before any account changes are made:
- Lemon Squeezy — verified with
LEMONSQUEEZY_WEBHOOK_SECRETvia HMAC-SHA256 - Stripe — verified with
STRIPE_WEBHOOK_SECRETusing Stripe's raw-body signature check
See Payment Webhooks for full setup details.
Trials
New accounts start on the free tier. The expires_at field tracks trial/subscription end dates. Background jobs monitor expiry and send warning emails ACCOUNT_EXPIRY_REMINDER_DAYS days before the deadline.