Skip to Content
Living documentation — last reviewed 2026-05-28
DecisionsADR-0008: Platform tiers as the feature-gate primitive

ADR-0008: Platform tiers as the feature-gate primitive

Status: Accepted Date: ~2026-02 (estimate) Context owner: Owner

Context

FitKit charges studios on a tiered plan. Each tier unlocks specific features (e.g., Spotter agent, custom minisites, advanced reporting). The implementation needs to:

  • Block API access to gated features for orgs not on the right tier.
  • Show the right upsell prompts in the UI (“Upgrade to use this”).
  • Survive a tier change (downgrade should immediately rescind access; upgrade should grant it without re-deploy).
  • Be observable: we want to know when an org hits a gate.

Decision

Introduce platform tiers as a first-class concept:

  • DB: platform_tiers table (definitions) + a tier column on organizations (current tier per org).
  • API: PlatformTierGuard registered as a global guard (after AuthGuard) in apps/api/src/app/app.module.ts. A method decorator on a controller route declares the required tier; the guard rejects with a structured error including the required tier and the org’s current tier.
  • Web: a <FeatureGate> component reads the user’s active org’s tier and either renders children or an <UpgradeCard> with the upsell copy. PostHog tracks every gate impression.
  • Spotter: tier surfaces are exposed to the agent as a tool, so it can answer “what would I get by upgrading?” with current truth.

Billing for tier changes goes through platform-billing (FitKit’s own Cardcom credentials, B2B charging).

Consequences

Positive

  • Single place to declare “this feature requires Tier X.” The decorator + the FeatureGate share the same enum.
  • Downgrade is instant: the next request fails the guard.
  • Server-side enforcement means client tampering can’t bypass.
  • PostHog data on gate-hit rate informs upsell pricing/positioning.

Negative

  • Adding a feature to a tier is a code change, not a config change. We could externalize tier definitions, but the upside is small for the cost.
  • Multi-org users can experience tier whiplash if they switch active orgs; the UI must show the active org’s tier clearly.
  • Tier enforcement at the route level doesn’t catch background jobs. Code that fires from a Bull job must re-check the org’s tier before doing tier-gated work.

Future considerations

  • Trial tier — currently implemented as a separate tier with the full feature set and a short expiry. Reconciling “trial vs paid” is the source of most edge-case bugs.
  • Feature flags vs tiers — tiers gate by package; feature flags would gate by individual feature. Today every feature is hardcoded to a tier; we’d add a flag layer if/when a feature needs gradual rollout independent of pricing.