Billing & Subscriptions
The platform uses Stripe as its payment processor, but subscription state lives in Keycloak rather than being queried from Stripe at runtime. This design keeps auth and entitlement checks fast (they hit Keycloak, which is already in the request path) and avoids rate-limiting issues with the Stripe API during high-traffic periods.
Stripe Integration
Stripe products map to platform tiers via metadata stored on the Stripe product object. The mapping is intentionally loose -- the platform reads the tier from product metadata rather than hardcoding Stripe product IDs. This means you can create new products in Stripe, set the right metadata, and the platform picks them up without a code change.
Subscription Tiers
| Tier | Agent Limit | Notes |
|---|---|---|
| Free | 1 | Default for all users, no payment required |
| Pro | 3 | Individual professionals (not recommended for starter teams -- see Team tier) |
| Team | 5 | Minimum for starter teams (each template has ~4 agents) |
| Crew | 10 | Two teams worth of agents |
| Enterprise | Unlimited | Custom pricing |
The Team tier exists because Pro's 3-agent limit is too small for starter teams, which typically include 4 agents each. If a user upgrades to Pro and immediately hits the limit trying to deploy a starter team, the experience is frustrating.
The Subscription Flow
When a user upgrades their plan, the following sequence plays out:
Why Keycloak Stores Subscription State
There are three common places to store subscription state: the application database, the auth provider, or a cache. Storing it in Keycloak (via KeycloakBillingService) was chosen because:
- Every authenticated request already hits Keycloak for token validation, so subscription info comes "for free" as user attributes in the token
- Feature gates resolve without extra database queries -- the
whoAmIGraphQL query returnssubscriptionInfodirectly from the user's Keycloak attributes - Stripe is the source of truth but not the runtime dependency -- if Stripe has an outage, existing subscriptions continue working because the state is cached in Keycloak
The whoAmI query returns a subscriptionInfo object that includes the product field (the Stripe product ID), which the UI uses to determine what features and limits apply.
Trial System
New users get a 14-day free trial tracked by a UserTrial entity in the database. During the trial:
- A banner is shown in the UI with the remaining days
- Agent creation and other gated operations check trial status
- Trial users get Team-tier limits (5 agents) so they can fully experience the platform
- When the trial expires, the user drops to Free-tier limits unless they subscribe
Trial enforcement happens at the operation level, not the UI level. Even if someone bypasses the frontend banner, the backend checks UserTrial before allowing gated operations like creating a new agent.
Cloud Desktop Billing
Cloud desktops (Linux and Windows VMs) are billed separately from the core subscription. They use dedicated Stripe products with per-device pricing. When a user provisions a cloud desktop:
- They are redirected to a dedicated pricing-table page for VM products
- After payment, the Stripe webhook processes the subscription
- The desktop VM is provisioned on Kubernetes (the cluster runs on GCP infrastructure)
- Usage is tracked per device, independent of the agent subscription tier
This separation exists because compute costs for VMs are significantly higher and more variable than the base platform, so bundling them into subscription tiers would either overprice the base tier or underprice the VMs.
Webhook Processing
The StripeWebhookResource handles several Stripe events:
checkout.session.completed-- initial subscription creationcustomer.subscription.updated-- plan changes, renewalscustomer.subscription.deleted-- cancellations
Each event updates the corresponding Keycloak user attributes. The webhook endpoint validates Stripe's signature to prevent spoofed events.
Webhook processing is idempotent by design. Stripe may send the same event multiple times (their docs recommend handling this), and the platform handles it gracefully by treating Keycloak attribute updates as upserts.
See Also
- Subscription Tiers -- detailed reference for each tier's limits and features
- Security Model -- how OIDC and Keycloak fit into the auth flow
- Org, Workspace & Project Hierarchy -- how subscriptions relate to the organizational model