| name | saas-entitlements-and-plan-gating |
| description | Use when designing the entitlements engine that enforces what each plan/tier/tenant can do — feature flags vs entitlements distinction, limits enforcement (seats, API calls, storage, projects, AI tokens), gate placement (UI, API, worker), upgrade-discovery UX, override mechanisms for enterprise contracts, and the runtime that resolves "can this tenant do X right now?" in a few milliseconds. |
| metadata | {"portable":true,"compatible_with":["Codex","codex"]} |
SaaS Entitlements and Plan Gating
Acknowledgement: Shared by Peter Bamuhigire, techguypeter.com, +256 784 464178.
Use When
- Designing the runtime that answers "what is this tenant allowed to do?" — every gated feature, every limit-checked action, every plan-tied capability.
- Replacing scattered
if tenant.plan == 'pro' checks with a single entitlements service.
- Designing the data model for plans, features, limits, overrides, and add-ons.
- Supporting enterprise custom-contract overrides without forking the codebase.
- Pairing in-product upgrade prompts (PLG) with the gate runtime so users hit a wall and see the path forward.
- Coordinating entitlements with feature flags (distinct concerns; they overlap dangerously if confused).
Do Not Use When
- The task is Stripe Billing primitives (Plan / Price / Subscription / Trial) — use
subscription-billing. This skill consumes those primitives.
- The task is experimentation feature flags (A/B tests, gradual rollout) — feature flags ≠ entitlements; see §3.
- The task is per-tenant data isolation — use
multi-tenant-saas-architecture.
- The task is enterprise SSO/SCIM — use
saas-sso-scim-enterprise-auth.
Required Inputs
- Plan catalogue (from
subscription-billing): tier names, features per tier, limits per tier.
- Pricing model decisions (per-seat, usage-based, hybrid) from product.
- List of features to gate + list of limits to enforce.
- Override policy: how enterprise contracts customise entitlements without code changes.
Workflow
- Read this
SKILL.md end-to-end.
- Distinguish entitlements (what plan allows) from feature flags (rollout/experimentation) — §3.
- Design the entitlements data model (§4) — plans, features, limits, overrides.
- Pick the resolution strategy (§5) — pre-resolved on JWT vs runtime lookup vs hybrid.
- Place the gates (§6) — UI hint, API enforce, worker enforce.
- Wire the upgrade-discovery UX (§7) — the gate is also a sales surface.
- Build the override mechanism (§8) for enterprise contracts.
- Instrument hits + denials (§9) for PLG analytics.
Quality Standards
- Entitlements resolution < 5ms p99 on the hot path (API request).
- Single source of truth for "which plan has what" — never duplicate in UI code and API code.
- Every denied action emits an event (for PQL scoring + upgrade prompts).
- Overrides logged in the audit log with actor + reason.
- Limit enforcement is atomic with the action — checking the limit and then performing the action races; use SELECT FOR UPDATE / Redis atomic counters / DB constraints.
Anti-Patterns
if (user.plan == 'pro') { … } scattered across the codebase. Fix: single resolver.
- Entitlements stored only in the front-end (the gate is bypassable). Fix: server-side enforcement is primary; UI mirrors.
- Counting limits via SELECT then INSERT (race condition). Fix: atomic counter or DB unique constraint per period.
- Feature flags and entitlements implemented in the same code path (a feature toggle ends up gating purchases). Fix: separate runtimes.
- Hard-coding "Enterprise" overrides as
if tenant.id == 12345 branches in the code. Fix: data-driven override table.
- No upgrade prompt when a gate denies (user hits a wall, doesn't know why). Fix: every denial response carries
upgrade_url + reason.
Outputs
- Plan × feature × limit catalogue (single source of truth).
- Entitlements resolver service + API.
- Gate placement map (UI / API / worker).
- Override schema and admin tool.
- Denial-event analytics dashboard (PQL surface).
Evidence Produced
| Category | Artifact | Format | Example |
|---|
| Architecture | Entitlements model spec | Markdown doc | docs/saas/entitlements-model.md |
| Release evidence | Plan × Feature × Limit matrix | Markdown table | docs/saas/plan-feature-matrix.md |
| Operability | Gate-denial analytics dashboard | Dashboard link | docs/saas/gate-denials-dashboard.md |
References
references/entitlements-vs-feature-flags.md — when each applies; how they coexist.
references/limit-enforcement-patterns.md — atomic counters, period rollover, distributed quotas.
references/enterprise-override-model.md — data-driven overrides for custom contracts.
- Companion:
subscription-billing, product-led-growth, saas-rate-limiting-and-quotas, saas-control-plane-engineering.
§1 What Entitlements Are
An entitlement is "this tenant is allowed to do X" — derived from the tenant's plan, contract overrides, add-ons, and trial state.
Examples:
- "Tenant on Pro plan can have up to 10 users."
- "Tenant on Free plan cannot use the API."
- "Tenant on Enterprise plan has unlimited storage."
- "Tenant on Pro+API_addon can call the API 100k times/month."
- "Tenant in trial has access to all Pro features."
The entitlements engine answers two questions:
- Boolean: "Can this tenant access feature X?" (feature gate)
- Limit: "Has this tenant exceeded its quota of Y this period?" (limit gate)
§2 The Three Inputs to a Resolution
plan baseline → plan_features (tier 'pro' has feature 'reporting')
plan_limits (tier 'pro' has limit 'users' = 10)
contract → tenant_overrides (tenant 123 has feature 'reporting' even on Free)
overrides tenant_overrides (tenant 123 has limit 'users' = 25 by negotiation)
add-ons → tenant_addons (tenant 123 purchased API add-on)
addon_features / addon_limits
Resolution precedence: override > add-on > plan baseline. Trial state is a special case — usually a tenant in trial inherits the target plan's entitlements.
§3 Entitlements vs Feature Flags
| Entitlements | Feature Flags |
|---|
| Purpose | Commercial gating (what plan allows) | Rollout / experimentation (who sees what) |
| Lifetime | Long-lived; tied to plan | Short-lived; removed after rollout |
| Audience | Tenants (by plan) | Users / tenants / cohorts (by criteria) |
| Source of truth | Billing system + entitlements DB | Feature-flag service (LaunchDarkly, Unleash, PostHog) |
| Default for new code | Inherit plan capabilities | Off (gradual rollout) |
| Audited? | Yes — overrides are commercial decisions | Lightly — flag state changes |
Keep them separate. Both runtimes can exist in the same app, but their data models, deploy cadence, and ownership differ. Commercial decisions don't belong in the feature-flag tool, and experimentation doesn't belong in the billing-driven entitlements DB.
§4 Data Model
CREATE TABLE plans (
code VARCHAR(32) PRIMARY KEY,
display_name VARCHAR(64),
trial_days INT,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE features (
code VARCHAR(64) PRIMARY KEY,
display_name VARCHAR(128),
description TEXT
);
CREATE TABLE plan_features (
plan_code VARCHAR(32) NOT NULL,
feature_code VARCHAR(64) NOT NULL,
PRIMARY KEY (plan_code, feature_code),
FOREIGN KEY (plan_code) REFERENCES plans(code),
FOREIGN KEY (feature_code) REFERENCES features(code)
);
CREATE TABLE limits (
code VARCHAR(64) PRIMARY KEY,
display_name VARCHAR(128),
period ENUM('lifetime','monthly','daily','none') NOT NULL DEFAULT 'lifetime',
unit VARCHAR(32)
);
CREATE TABLE plan_limits (
plan_code VARCHAR(32) NOT NULL,
limit_code VARCHAR(64) NOT NULL,
value BIGINT,
PRIMARY KEY (plan_code, limit_code)
);
CREATE TABLE tenant_feature_overrides (
tenant_id BIGINT UNSIGNED NOT NULL,
feature_code VARCHAR(64) NOT NULL,
enabled BOOLEAN NOT NULL,
reason VARCHAR(128),
expires_at DATETIME,
granted_by BIGINT UNSIGNED,
granted_at DATETIME NOT NULL,
PRIMARY KEY (tenant_id, feature_code)
);
CREATE TABLE tenant_limit_overrides (
tenant_id BIGINT UNSIGNED NOT NULL,
limit_code VARCHAR(64) NOT NULL,
value BIGINT,
reason VARCHAR(128),
expires_at DATETIME,
granted_by BIGINT UNSIGNED,
granted_at DATETIME NOT NULL,
PRIMARY KEY (tenant_id, limit_code)
);
CREATE TABLE addons (
code VARCHAR(64) PRIMARY KEY,
display_name VARCHAR(128)
);
CREATE TABLE addon_features (
addon_code VARCHAR(64) NOT NULL,
feature_code VARCHAR(64) NOT NULL,
PRIMARY KEY (addon_code, feature_code)
);
CREATE TABLE addon_limits (
addon_code VARCHAR(64) NOT NULL,
limit_code VARCHAR(64) NOT NULL,
value_delta BIGINT NOT NULL,
PRIMARY KEY (addon_code, limit_code)
);
CREATE TABLE tenant_addons (
tenant_id BIGINT UNSIGNED NOT NULL,
addon_code VARCHAR(64) NOT NULL,
quantity INT NOT NULL DEFAULT 1,
starts_at DATETIME NOT NULL,
ends_at DATETIME,
PRIMARY KEY (tenant_id, addon_code)
);
CREATE TABLE tenant_usage_counters (
tenant_id BIGINT UNSIGNED NOT NULL,
limit_code VARCHAR(64) NOT NULL,
period_key VARCHAR(16) NOT NULL,
used BIGINT NOT NULL DEFAULT 0,
updated_at DATETIME NOT NULL,
PRIMARY KEY (tenant_id, limit_code, period_key)
);
§5 Resolution Strategies
| Strategy | Latency | Freshness | Use case |
|---|
| JWT-embedded | < 0.1ms (no I/O) | Until token refresh (15min) | Boolean feature checks; tier-level limits |
| Runtime DB lookup | 1-5ms | Real-time | Limit checks needing current counter; just-changed plans |
| Redis cache + DB fallback | < 1ms hit / 5ms miss | Configurable TTL | High-traffic feature checks |
| Hybrid | Mixed | Mixed | Default: boolean checks in JWT, limit checks against Redis counter |
Default architecture:
Auth issues JWT with: plan, features[], limits[] (snapshot at issue)
On every request:
- Boolean feature check: read JWT (free, instant)
- Limit check: Redis INCR + compare against JWT limit (atomic, fast)
- Periodic re-issue (15min) refreshes JWT against DB
On plan change / override:
- Invalidate user's JWT (force re-issue)
- Update DB
- Emit `entitlements.changed` event
§6 Gate Placement
UI gates — Hide / disable features the user can't access. NEVER trust as enforcement.
Show "Upgrade to Pro" CTAs.
API gates — Primary enforcement. Reject with 402 Payment Required or 403 Forbidden + upgrade info.
Worker gates — Background jobs must check too. A bypass of API gates via direct queue insertion is an exploit.
DB constraints — For hard limits (max users per tenant), enforce at DB level too.
Pattern for an API gate:
@require_entitlement(feature='api_access')
@require_within_limit(limit='api_calls_monthly')
def api_endpoint(request):
Entitlements.consume_limit(tenant_id, 'api_calls_monthly', amount=1)
Denial response:
{
"error": {
"code": "ENTITLEMENT_REQUIRED",
"feature": "api_access",
"message": "API access is available on the Pro plan and above.",
"upgrade_url": "https://app.example.com/billing/upgrade?target=pro&context=api"
}
}
§7 Upgrade-Discovery UX
The gate is a sales surface, not just a wall. Every denial:
- Tells the user what they tried to do and why it's blocked.
- Names the required plan explicitly.
- Links to one-click upgrade with deep-link context (so post-upgrade they land back where they were).
- Emits a
gate.denied event for PQL scoring + lifecycle email.
In-product approaching-limit UX:
- 70% of limit: passive indicator (progress bar).
- 80%: in-app banner suggesting upgrade.
- 95%: stronger CTA + email.
- 100%: hard block with one-click upgrade + email.
§8 Enterprise Override Model
Enterprise contracts often grant custom entitlements. Implement as data, never as code branches:
Admin Console → POST /admin/tenants/123/entitlements
body: { feature: 'sso_saml', enabled: true, reason: 'Q2 contract Acme', expires_at: '2027-04-01' }
→ writes to tenant_feature_overrides
→ writes audit_log entry
→ emits entitlements.changed event
Read-time resolution merges plan + add-ons + overrides automatically.
§9 Analytics — The Gate Is the PQL Source
Every gate denial is a Product-Qualified-Lead signal. Materialize:
gate_denials_per_tenant_per_day (with feature/limit, severity)
tenants_hitting_limit_80pct (the upgrade pipeline)
feature_lock_topN (which features drive the most upgrade-intent signal)
Feed into product-led-growth skill's PQL scoring; coordinate with saas-lifecycle-email-orchestration (don't email and in-app prompt the same denial — pick one or stagger).
§10 Anti-Patterns
- Single big
plans.json file in the repo consumed by both UI and API at compile time — drift, redeploys to change limits.
- No override audit trail — "who gave Acme unlimited seats?" unanswerable. Fix: every override writes audit_log.
- Limit check via
SELECT COUNT(*) on every request — slow + race-condition prone. Fix: maintained counter + atomic INCR.
- No expiry on overrides — a promo grant becomes permanent by accident. Fix:
expires_at mandatory for promo overrides.
- No "upgrade context" preserved through the upgrade flow — user clicks upgrade, lands on pricing page, forgets why. Fix:
upgrade_url carries context= param; post-upgrade redirects back to original action.
§11 Read Next
subscription-billing — plan / price / subscription primitives.
product-led-growth — PQL + activation; coordinate with gates.
saas-rate-limiting-and-quotas — runtime quota enforcement.
saas-control-plane-engineering — the admin tool that issues overrides.
saas-transactional-email-infrastructure + saas-lifecycle-email-orchestration — coordinate gate denials with upgrade email sequences.
AI Entitlements Addendum
When the SaaS includes AI features, the entitlement catalogue extends with AI-specific keys (model tier, context length, generations/day, tools allow-list, KB size, agent steps, BYOK, fine-tune). The mapping and gateway-enforcement contract live in ai-entitlements-and-feature-gating.
Cross-references:
ai-entitlements-and-feature-gating — AI-specific entitlement keys + gateway enforcement.
ai-usage-metering-and-billing — what is gated vs metered.
ai-model-gateway — runtime enforcement chokepoint.
ai-on-saas-architecture — control-plane positioning.