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_tierstable (definitions) + a tier column onorganizations(current tier per org). - API:
PlatformTierGuardregistered as a global guard (afterAuthGuard) inapps/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.
Related
- features/platform-billing/README.md
- features/spotter-agent/README.md — agent surfaces tier context
- architecture/api.md — guard chain