Skip to main content

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

TierAgent LimitNotes
Free1Default for all users, no payment required
Pro3Individual professionals (not recommended for starter teams -- see Team tier)
Team5Minimum for starter teams (each template has ~4 agents)
Crew10Two teams worth of agents
EnterpriseUnlimitedCustom pricing
info

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:

  1. Every authenticated request already hits Keycloak for token validation, so subscription info comes "for free" as user attributes in the token
  2. Feature gates resolve without extra database queries -- the whoAmI GraphQL query returns subscriptionInfo directly from the user's Keycloak attributes
  3. 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
note

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:

  1. They are redirected to a dedicated pricing-table page for VM products
  2. After payment, the Stripe webhook processes the subscription
  3. The desktop VM is provisioned on Kubernetes (the cluster runs on GCP infrastructure)
  4. 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 creation
  • customer.subscription.updated -- plan changes, renewals
  • customer.subscription.deleted -- cancellations

Each event updates the corresponding Keycloak user attributes. The webhook endpoint validates Stripe's signature to prevent spoofed events.

warning

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