Subscription plans
Each application can have multiple subscription plans. A plan carries the features it includes, as plain values, in a features object that is injected as a features claim in JWT access tokens, allowing downstream services to gate functionality without querying auth-service. What a plan charges, and the features billed by usage, are its prices.
Plan schema
| Field | Type | Default | Description |
|---|---|---|---|
id | UUID | auto | Primary key |
applicationId | UUID | required | Application this plan belongs to |
name | string | required | Plan name (unique per application, max 64 chars) |
description | string | — | Optional description |
stripeProductId | string | — | Stripe Product ID, set by auth-service (read-only) |
features | JSON object | {} | Included features: each value a string, a number or a boolean, e.g. {"maxProjects": 10, "apiAccess": true}. A metered feature is not declared here (see prices) |
isDefault | boolean | false | Automatically assigned when a user is granted access to the app and no plan is specified |
Only one plan per application can have isDefault = true.
Plan prices
A plan can have multiple prices (monthly, annual, one-time):
| Field | Type | Default | Description |
|---|---|---|---|
id | UUID | auto | Primary key |
planId | UUID | required | Parent plan |
name | string | required | Price label, e.g. Monthly |
amount | numeric | required | Amount in smallest currency unit (e.g. cents) |
currency | string | usd | 3-letter currency code |
interval | "month" | "year" | "one_time" | month | Billing interval (one_time is refused on a metered price) |
billingType | "fixed" | "metered" | fixed | A fixed price is the charge of the plan; a metered price bills one unit of a feature |
featureKey | string | — | Metered only (required there): the consumption key the price bills |
unit | string | — | Metered only (required there): label of one unit |
usageLimit | integer | — | Metered only (required there): cap of the billing period in units, -1 for unlimited |
stripePriceId | string | — | Stripe Price ID, set by auth-service (read-only) |
Stripe integration
When STRIPE_SECRET_KEY is set:
- Creating a plan via
POST /api/admin/applications/:appId/plansalways creates a Stripe Product, named<AppName> — <PlanName>. A caller cannot supply a Stripe id: a request that does is refused. - Creating a price via
POST /api/admin/applications/:appId/plans/:planId/pricescreates a Stripe Price on that product. A metered price also gets a Stripe Meter, shared by every plan of the application that bills the same feature.
Without STRIPE_SECRET_KEY the Stripe calls are skipped and the plan and price rows are saved without Stripe ids. The request and response shapes of the plan and price endpoints are described in Managing plans; the Stripe side in the Stripe integration guide.
Admin API endpoints
List plans with prices
GET /api/admin/applications/:appId/plans
Returns all plans with their associated prices:
{
"plans": [
{
"id": "...",
"name": "pro",
"features": { "maxProjects": 50, "apiAccess": true },
"isDefault": false,
"prices": [
{ "name": "Monthly", "amount": 1500, "currency": "usd", "interval": "month" }
]
}
]
}
Create a plan
POST /api/admin/applications/:appId/plans
{
"name": "pro",
"description": "Professional tier",
"features": { "maxProjects": 50, "apiAccess": true },
"isDefault": false
}
Update a plan
PATCH /api/admin/applications/:appId/plans/:planId
Accepts name, description, features and isDefault. Every field is optional and an omitted one is left unchanged. features replaces the whole object.
Delete a plan
DELETE /api/admin/applications/:appId/plans/:planId
Blocked if any active user is subscribed to the plan (400 SUB_003).
Create a price
POST /api/admin/applications/:appId/plans/:planId/prices
{
"name": "Monthly",
"amount": 1500,
"currency": "usd",
"interval": "month"
}
Update a price
PATCH /api/admin/applications/:appId/plans/:planId/prices/:priceId
Only name can change on a fixed price; a metered price also accepts unit and usageLimit.
Delete a price
DELETE /api/admin/applications/:appId/plans/:planId/prices/:priceId
The Stripe Price is archived.
Assigning a subscription to a user
Subscriptions are managed via the user access endpoint:
PATCH /api/admin/applications/:appId/users/:userId
{
"subscriptionPlanId": "<plan-uuid>",
"reason": "Customer upgrade"
}
user_subscriptions is the source of truth (it feeds the features claim);
user_applications.subscription_plan_id is a denormalised mirror kept in sync
by a single write path. Setting subscriptionPlanId to null revokes the
subscription. A manual change requires a reason. An optional expiresAt sets
or clears the expiry; omitting it leaves the current expiry untouched.
Every effective change appends a row to the user_subscription_events audit
table and, except for the automatic assignment of the default plan, publishes a
subscription.changed live event. Re-assigning the same plan is a no-op and is
not recorded.
Gateway teams
When an application has LiteLLM support, each of its plans maps to a gateway team named <applicationSlug>:<planSlug> — the plan name lower-cased with every non-alphanumeric run replaced by -, so Premium becomes acme-app:premium. The team is created (closed) when the plan is created, replaced when the plan is renamed, and blocked when the plan is deleted; deleting the application removes all its teams. Creating or renaming a plan whose slug would collide with another plan of the same application is refused (400 APP_001), whether or not the application has LiteLLM support.
Each team carries an empty Access Group of the same name, where a gateway administrator grants the models, MCP servers and agents that plan may use. The plan a user holds for the application (their subscription, or the default plan) decides which team their gateway calls run under. See LiteLLM gateway.
Feature claims in tokens
When a client requests the features scope, the access token contains:
{
"features": { "maxProjects": 50, "apiAccess": true },
"plan": "pro"
}
The features value is the features JSON from the user's active subscription plan, and plan is the plan name. If the user has no active subscription, features is {} and there is no plan claim; the same applies when the subscription has expired.