Concepts

Subscription plans

Subscription plans, feature flags, and Stripe billing integration in auth-service.

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

FieldTypeDefaultDescription
idUUIDautoPrimary key
applicationIdUUIDrequiredApplication this plan belongs to
namestringrequiredPlan name (unique per application, max 64 chars)
descriptionstring—Optional description
stripeProductIdstring—Stripe Product ID, set by auth-service (read-only)
featuresJSON 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)
isDefaultbooleanfalseAutomatically 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):

FieldTypeDefaultDescription
idUUIDautoPrimary key
planIdUUIDrequiredParent plan
namestringrequiredPrice label, e.g. Monthly
amountnumericrequiredAmount in smallest currency unit (e.g. cents)
currencystringusd3-letter currency code
interval"month" | "year" | "one_time"monthBilling interval (one_time is refused on a metered price)
billingType"fixed" | "metered"fixedA fixed price is the charge of the plan; a metered price bills one unit of a feature
featureKeystring—Metered only (required there): the consumption key the price bills
unitstring—Metered only (required there): label of one unit
usageLimitinteger—Metered only (required there): cap of the billing period in units, -1 for unlimited
stripePriceIdstring—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/plans always 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/prices creates 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.

Copyright © 2026