SaaS Entitlements Architecture: Plans, Features, Quotas, Trials, Add-Ons, and Why Billing Isn’t Authorization
A SaaS product often starts with a harmless-looking check:
if customer.plan == "pro":
show_advanced_analytics()
That works until the business becomes real.
Then sales closes an enterprise customer that needs one Premium feature on a custom contract. A trial should unlock most paid features for 14 days. One customer is grandfathered onto an old plan. Another buys an add-on. A third exceeds a seat limit. A support engineer grants temporary access. A subscription downgrade should take effect next month, not now. A failed payment should revoke some features after a grace period. An AI feature has a monthly quota instead of a simple on/off switch.
Suddenly, the question is no longer:
> Which plan is this customer on?
The real question is:
> What is this tenant entitled to use right now, under the current commercial contract and product rules?
That is the job of an entitlements layer.
Entitlements sit between billing and your application. They translate products, subscriptions, trials, add-ons, negotiated contracts, limits, and exceptions into a stable set of product capabilities that the application can enforce.
This distinction matters because billing is not authorization, authorization is not entitlements, and feature flags are not automatically all three.
Modern SaaS platforms are increasingly making this separation explicit. Stripe Billing maps product features to customer entitlements. Clerk Billing attaches Features to Plans and can enforce plan and seat-based access. WorkOS exposes organization/user feature flags through authentication and runtime evaluation. LaunchDarkly documents long-lived entitlement targeting separately from temporary release flags.
The common idea is simple:
> Commercial state changes frequently. Product capabilities should have stable identifiers. Application code should ask for capabilities, not reimplement pricing logic.
This guide explains how to design that architecture without turning a normal SaaS backend into a giant policy platform.
TL;DR
A strong SaaS access model separates four different concerns:
| Concern | Question it answers |
|---|---|
| Billing | What did the customer buy, and what should they be charged? |
| Entitlements | What product capabilities and limits does the customer receive? |
| Authorization | May this specific actor perform this action on this resource? |
| Feature flags | Should this code path or product experience be enabled for this target/environment? |
A practical architecture looks like:
Billing / contracts / trials / add-ons
↓
Entitlement resolver
↓
Tenant entitlement snapshot
↓
Application checks
↓
Authorization + resource policy
↓
Protected operation
For quotas and usage-limited features:
entitlement says:
ai_runs = 1,000 / month
usage system says:
consumed = 742
runtime decision:
remaining = 258
Core design rules:
- Use stable feature keys instead of plan-name checks throughout the codebase.
- Resolve entitlements at the tenant/account level for B2B SaaS.
- Keep billing-provider objects out of business logic where possible.
- Model more than booleans: quantities, limits, variants, and metadata can be entitlements.
- Support explicit overrides without mutating the base plan.
- Version plan packaging and keep grandfathered customers reproducible.
- Treat trials and add-ons as sources of entitlements, not special-case UI behavior.
- Keep usage counters separate from entitlement definitions.
- Enforce paid capabilities on the server; frontend hiding is only UX.
- Design for billing-webhook delay and stale entitlement caches.
- Make downgrade, cancellation, failed-payment, and grace-period semantics explicit.
- Audit why an entitlement was granted or denied.
- Do not build a separate microservice unless scale or ownership actually requires one.
The Fundamental Mistake: Using the Plan Name as Product Logic
Many SaaS applications begin with code like:
if plan == "basic":
...
elif plan == "pro":
...
elif plan == "premium":
...
This couples two things that evolve at different speeds:
- your commercial packaging;
- your product capabilities.
Marketing may rename Pro to Growth.
Sales may create Enterprise Plus.
Finance may introduce annual pricing.
A legacy customer may keep an old Pro 2025 plan.
A customer may buy one add-on without upgrading.
Your application should not care about most of that.
The application should care about capabilities such as:
advanced_analytics
export_csv
custom_branding
ai_assistant
priority_support
api_access
max_team_members
monthly_ai_credits
data_retention_days
custom_domains
That creates a stable contract between product code and commercial configuration.
A pricing change can then become:
New Growth Plan
advanced_analytics = true
export_csv = true
api_access = true
max_team_members = 25
monthly_ai_credits = 5000
without rewriting every controller, worker, background job, and frontend component that once checked for plan == pro.
Billing, Entitlements, Authorization, and Feature Flags Are Different
These concepts overlap, which is why teams often collapse them too early.
Billing
Billing answers:
- Which product or plan did the customer purchase?
- How much should they pay?
- When does the subscription renew?
- Is there a trial?
- Is there a discount?
- Is the subscription active, canceled, past due, or scheduled to change?
- How many seats or usage units should be invoiced?
Billing is commercial state.
Stripe, Paddle, Chargebee, Clerk Billing, or your own subscription system can own parts of this state.
Entitlements
Entitlements answer:
- Does this account have Advanced Analytics?
- How many seats may this organization have?
- Is API access enabled?
- How many AI credits are included?
- What data-retention limit applies?
- Is custom branding available?
- Which product variant should this tenant receive?
Entitlements are product-access state derived from commercial and operational inputs.
Authorization
Authorization answers:
> May this actor perform this action on this resource?
An organization may be entitled to Advanced Analytics, while a normal employee inside that organization is still not authorized to change analytics configuration.
Conceptually:
Tenant entitlement:
advanced_analytics = enabled
User authorization:
user has analytics:view = yes
user has analytics:configure = no
Both checks matter.
An entitlement grants capability to the account.
Authorization decides which principals within that account may exercise it.
Feature flags
Feature flags typically control release or runtime behavior:
- beta rollout
- canary exposure
- experiment cohort
- emergency kill switch
- percentage rollout
- environment-specific enablement
- temporary migration path
They can also implement entitlements, especially when the flag system supports long-lived targeting. LaunchDarkly explicitly documents entitlement use as a long-lived targeting pattern.
But lifecycle matters.
A temporary release flag may disappear when rollout completes.
A paid entitlement may need to remain stable for years because it is part of a customer contract.
Treating every entitlement as an ordinary temporary release flag can create operational debt unless the flag system supports permanent, audited entitlement semantics.
The Product Capability Is the Stable Boundary
A good entitlement key should describe a product capability, not a pricing package.
Good:
analytics.advanced
reports.export
api.access
branding.custom
support.priority
ai.assistant
seats.max
retention.days
projects.max
Fragile:
is_pro
premium_user
plan_3
new_pricing_customer
enterprise_2026
Why?
Because product capabilities are relatively stable.
Packaging changes constantly.
Today:
Pro:
analytics.advanced
reports.export
Next quarter:
Growth:
reports.export
Business:
reports.export
analytics.advanced
If the application checks capability keys, no business logic changes.
Only packaging configuration changes.
That is the leverage an entitlement layer gives you.
Not Every Entitlement Is Boolean
A common entitlement model starts as:
feature → true / false
That is useful, but insufficient for many SaaS products.
A production model often needs several types.
Boolean capability
custom_branding = true
api_access = true
Quantity limit
max_team_members = 25
max_projects = 100
monthly_exports = 500
Usage allowance
monthly_ai_credits = 10000
monthly_api_calls = 100000
Configuration value
data_retention_days = 365
max_upload_size_mb = 500
Variant
support_tier = priority
analytics_tier = advanced
model_access = premium
You do not need an infinitely generic type system.
Support the entitlement shapes your product genuinely needs.
The dangerous alternative is forcing every commercial rule into booleans and then inventing dozens of artificial feature names such as:
seats_10
seats_25
seats_50
seats_unlimited
when a quantity would be clearer.
A Practical Entitlement Data Model
A simple relational design can handle a surprising amount of complexity.
Conceptually:
features
--------
id
key
type
description
plans
-----
id
key
version
status
plan_entitlements
-----------------
plan_id
feature_id
boolean_value
numeric_value
string_value
subscriptions
-------------
tenant_id
plan_id
status
effective_from
effective_until
tenant_entitlement_overrides
----------------------------
tenant_id
feature_id
operation
value
effective_from
effective_until
reason
add_on_entitlements
-------------------
tenant_id
feature_id
value
effective_from
effective_until
source
entitlement_snapshots
---------------------
tenant_id
version
resolved_payload
resolved_at
This is only a conceptual shape.
You may not need each table separately.
The important idea is that the resolved entitlement is derived from multiple sources while the application receives one predictable answer.
The Entitlement Resolver
The resolver combines all relevant entitlement sources.
For one tenant:
base plan
+ add-ons
+ trial grants
+ negotiated contract grants
+ tenant overrides
+ temporary support grants
- explicit restrictions
= resolved entitlement set
For example:
Base Growth plan:
api.access = true
projects.max = 25
analytics.advanced = false
Analytics add-on:
analytics.advanced = true
Enterprise contract override:
projects.max = 100
Temporary support grant:
support.priority = true until Oct 1
Resolved:
api.access = true
projects.max = 100
analytics.advanced = true
support.priority = true
Application code should ideally consume the resolved state instead of repeating precedence rules everywhere.
Define Precedence Explicitly
Overrides become dangerous when precedence is implicit.
Suppose:
- plan allows 10 seats;
- add-on adds 20;
- sales override says 50;
- temporary restriction says 15.
Which wins?
You need a documented rule.
A simple model might be:
explicit deny/restriction
>
tenant override
>
add-on
>
base plan
>
default
But quantity entitlements may require different operations:
replace
increment
minimum
maximum
Example:
base plan seats = 10
add-on seats +20
contract override minimum 50
resolved seats = 50
Avoid inventing a sophisticated policy language unless real product requirements demand it.
For many SaaS products, a small number of explicit override operations is enough.
Tenant-Level Entitlements Are Usually the Right Default for B2B SaaS
In multi-tenant B2B software, the commercial customer is usually an organization, workspace, restaurant, company, or account.
That means entitlements normally belong to:
tenant / organization / account
not directly to an individual user.
A user accesses the tenant through membership.
The request path becomes:
authenticated user
↓
resolve active tenant
↓
load tenant entitlements
↓
check tenant has capability
↓
check user is authorized
↓
perform action
This avoids duplicating commercial state across every member.
User-specific grants can still exist, but they should be intentional exceptions.
Server-Side Enforcement Is Mandatory
A frontend check like:
if hasFeature("advanced_analytics"):
show_button()
improves UX.
It is not security.
The API must enforce the same capability.
Otherwise a user can call the protected endpoint directly.
For sensitive operations:
request
↓
authentication
↓
tenant resolution
↓
entitlement check
↓
authorization check
↓
business validation
↓
operation
The exact order can vary, but both entitlement and authorization must be enforced in trusted server-side code.
This is especially important for:
- exports
- expensive AI calls
- premium APIs
- data-retention operations
- custom-domain management
- integrations
- storage limits
- team/seat creation
- administrative actions
Entitlements Are Not Permissions
This distinction deserves its own section because it prevents subtle security bugs.
Suppose the Business plan includes invoice management.
That does not mean every user can edit invoices.
Think of it as two gates:
Gate 1 — Account entitlement:
Does this organization have invoice management?
Gate 2 — Actor authorization:
May this particular user edit this invoice?
Possible result:
organization entitlement = yes
user role = viewer
invoice.edit = denied
Do not encode roles into plan names.
Do not assume "paid user" means "authorized user."
Commercial access and security policy are separate dimensions.
Clerk's current model illustrates this relationship: Features can be attached to billing Plans, while permissions still determine actions within those features. Its B2B documentation explicitly ties custom permissions to Features that must also exist in the organization's active Plan.
Entitlements Are Not Just Feature Flags Either
Feature flags and entitlements can use the same evaluation infrastructure, but they often have different lifecycle semantics.
Release flag
new_editor_v2
purpose: rollout
owner: engineering
lifetime: weeks
eventual destination: delete
Entitlement flag
advanced_analytics
purpose: commercial capability
owner: product / billing
lifetime: years
eventual destination: stable contract
LaunchDarkly describes entitlement flags as permanent or long-lived targeting rules.
This distinction helps with flag hygiene.
Temporary release flags should usually be removed after rollout.
Paid entitlement keys often become part of a long-term product contract and should be treated as stable API-like identifiers.
Plans Should Be Packaging, Not Enforcement Code
A plan is a convenient bundle.
Basic
feature A
feature B
Pro
feature A
feature B
feature C
limit X = 50
Premium
feature A
feature B
feature C
feature D
limit X = unlimited
The application should not need to know the bundle name.
It should ask:
has feature C?
what is limit X?
That makes it possible to:
- rename plans;
- add regional pricing;
- create annual vs monthly prices;
- run packaging experiments;
- create private enterprise plans;
- grandfather older customers;
- attach add-ons;
- migrate billing vendors.
This is exactly the type of decoupling Stripe Entitlements is designed to support: product features are mapped to Stripe Products, and customer active entitlements are derived from subscription status and those mappings.
Trials Should Grant Entitlements, Not Bypass Checks
A fragile trial implementation often looks like:
if user.is_trial:
allow_everything()
That creates a second access-control system.
A cleaner model is:
Trial source:
analytics.advanced = true
api.access = true
projects.max = 50
expires_at = trial_end
The normal entitlement resolver includes those grants until expiry.
When the trial ends, the resolved entitlement set changes.
No feature endpoint needs special trial logic.
Clerk Billing similarly treats trials as a commercial state that can provide paid Feature access for a time-boxed period.
Add-Ons Should Compose With the Base Plan
Add-ons are one of the first things that break plan-name checks.
Example:
Growth plan
+
Advanced Analytics add-on
+
Extra 20 seats
If code says:
advanced analytics requires Business plan
the add-on cannot work without special casing.
With entitlements:
Growth:
analytics.advanced = false
seats.max = 10
Analytics add-on:
analytics.advanced = true
Seat add-on:
seats.max += 20
Resolved result:
analytics.advanced = true
seats.max = 30
The rest of the application remains unchanged.
Custom Enterprise Deals Need Overrides
Sales-assisted SaaS eventually creates non-standard contracts.
Examples:
- 200 seats instead of 100;
- one Premium feature on a lower plan;
- unlimited exports;
- 730-day retention;
- no AI access for compliance;
- custom API quota;
- temporary migration allowance.
Do not create a brand-new hardcoded plan for every customer.
Use tenant-level overrides linked to:
- who created the override;
- why it exists;
- when it begins;
- when it expires;
- contract or ticket reference where appropriate.
Example:
tenant: acme
feature: retention.days
operation: replace
value: 730
effective_until: contract_end
reason: enterprise agreement
Overrides should be visible in support/admin tooling.
Hidden overrides are how entitlement systems turn into archaeology.
Grandfathered Plans Need Versioned Packaging
Imagine Pro originally included:
api.access = true
projects.max = 100
Later you change Pro to:
api.access = false
projects.max = 50
Do existing customers lose access immediately?
Maybe not.
This is a commercial decision, but the architecture must support it.
Do not mutate history and then rely on memory.
Use plan/package versions or effective-dated entitlement mappings.
Conceptually:
pro_v1
effective: 2025-01-01 → 2026-06-30
pro_v2
effective: 2026-07-01 →
Existing customers can remain on v1 until intentionally migrated.
New customers receive v2.
This makes grandfathering reproducible.
It also makes support questions answerable:
> Why does this customer have API access when the current Pro page says they should not?
Because their subscription is bound to the earlier packaging version.
Upgrades and Downgrades Need Effective-Time Semantics
Not every subscription change should affect access immediately.
Immediate upgrade
Common behavior:
payment succeeds
→ new subscription state
→ higher entitlements become active
Scheduled downgrade
Common behavior:
customer requests downgrade today
current paid period continues
lower entitlements activate at renewal
Cancellation at period end
subscription marked cancel_at_period_end
entitlements stay active until effective cancellation
Immediate cancellation
Some products revoke immediately; others apply a grace period.
The entitlement resolver should evaluate effective state, not merely the last button the customer clicked.
Never scatter date logic across every feature endpoint.
Failed Payments and Grace Periods Must Be Explicit
What happens when a renewal payment fails?
Possible policies:
- revoke paid features immediately;
- allow a 3-day grace period;
- keep read access but block writes;
- keep existing data accessible but stop expensive AI/API usage;
- downgrade after subscription enters a specific terminal state.
There is no universal correct policy.
But there must be one authoritative policy.
Example:
subscription status = past_due
grace_period_end = Sep 23
until grace_period_end:
paid entitlements remain active
after grace_period_end:
compute free-plan entitlements
Do not let every service interpret billing statuses independently.
That produces inconsistent product behavior.
Quotas Are Entitlements Plus Usage
A quota is not just an entitlement.
Suppose:
monthly_exports = 1,000
The entitlement tells you the allowed quantity.
You still need usage state:
consumed_exports = 742
The decision is:
limit = entitlement(monthly_exports)
used = usage_counter(monthly_exports)
allow if used < limit
This is the same principle used in AI SaaS billing.
Entitlements describe what the customer is allowed.
Metering records what they consumed.
Keep those systems related but separate.
Do Not Put Mutable Usage Counters Inside the Entitlement Definition
Avoid this model:
entitlement:
ai_credits_remaining = 287
if the number changes on every request.
That turns your entitlement catalog into a high-write accounting store.
Prefer:
entitlement:
monthly_ai_credits = 1000
usage:
consumed_this_period = 713
derived:
remaining = 287
This keeps commercial configuration stable while the usage ledger handles consumption.
A cached materialized "remaining" value can exist for performance, but it should be derived from authoritative allowance and usage state.
Seat Limits Are a Great Example of Quantitative Entitlements
Seat-based SaaS often needs two separate concepts:
commercial seats purchased
membership limit allowed
Clerk's current Billing documentation reflects this directly: its seat-based Plans can enforce membership limits, per-seat pricing, included seats, and plan-specific seat caps.
In your own architecture, a seat entitlement might be:
seats.max = 25
When inviting a member:
current_members + pending_invitations
<
seats.max
If false:
- block invitation;
- show upgrade path;
- optionally allow seat purchase;
- do not create a membership and hope billing catches up later.
The enforcement point should be the operation that changes seat occupancy.
Entitlement Resolution Should Have a Single Semantic Source of Truth
A common failure mode is duplicated meaning.
Frontend:
Pro has analytics
API:
Business has analytics
Background exporter:
everyone except Free has analytics
Admin panel:
checks subscription price ID
This is not four implementations.
It is four future bugs.
Create one semantic API or shared library:
has_entitlement(tenant, "analytics.advanced")
get_entitlement_limit(tenant, "projects.max")
This does not require every call to cross the network.
You can centralize meaning while distributing cached evaluation.
Centralized Semantics Do Not Require an Entitlements Microservice
Do not overengineer the first implementation.
For many SaaS products, the best architecture is:
modular monolith
├── billing module
├── entitlement module
├── authorization module
├── usage module
└── domain modules
The entitlement module can own:
- feature catalog;
- plan mappings;
- override resolution;
- snapshots;
- invalidation;
- audit metadata.
Extract a standalone service when you have real reasons:
- many independent services need low-latency evaluation;
- different teams own packaging and product access;
- entitlement updates are high volume;
- you need cross-language clients;
- the entitlement engine becomes operationally independent.
A network service is not inherently more scalable than a clean module.
Event-Driven Synchronization Is Useful, but Billing Webhooks Are Not Instant Truth
If a billing provider owns subscription state, changes usually arrive through webhooks or API reads.
The path may look like:
customer upgrades
↓
billing provider
↓
webhook
↓
subscription projection
↓
entitlement recompute
↓
cache invalidation
↓
application sees new capability
This introduces eventual consistency.
Your product must decide how much delay is acceptable.
After checkout
A customer who just paid expects access immediately.
Possible strategy:
- confirm checkout/subscription success;
- synchronously fetch or derive the new subscription state when safe;
- update local entitlement projection;
- let webhooks remain the durable reconciliation path.
In normal operation
Use webhooks/events to keep state synchronized.
During provider outage
Do not make every request depend synchronously on the billing provider.
Your SaaS should continue evaluating known entitlements from local durable state or cache.
Stripe Entitlements: Useful Boundary, Not a Replacement for All Product Policy
Stripe Billing Entitlements maps internal service features to Stripe Products and provides active entitlements based on customer subscriptions.
That is useful because the billing catalog can directly express:
product → features
customer subscription → active features
Stripe exposes active entitlements per customer and notifies applications when product access should be provisioned or de-provisioned.
But your application may still need additional policy such as:
- internal free accounts;
- support overrides;
- usage-based limits;
- tenant-specific contracts;
- temporary operational restrictions;
- non-Stripe product sources;
- authorization inside each feature.
Treat external entitlement systems as an input or authoritative source for the scope they own—not as an excuse to collapse unrelated security and product rules.
Clerk: Plans, Features, Seats, and Authorization
Clerk's current Billing model exposes Plans and Features and supports server-side access checks.
Its seat-based Plans can combine:
- base fees;
- per-seat charges;
- included seats;
- seat limits.
This is a useful example of why "subscription active" is too weak as an application primitive.
The product needs to know:
feature available?
seat quantity allowed?
user permission allowed?
Those are related but not identical facts.
Clerk's documentation also notes that custom permissions tied to a Feature only succeed when that Feature exists in the active Organization Plan—an explicit example of entitlement and authorization working together rather than being the same thing.
WorkOS and LaunchDarkly Show the Runtime Side
WorkOS Feature Flags can target users and organizations and expose enabled flags through access tokens or runtime evaluation. Its docs explicitly include premium-feature access as a use case.
That demonstrates a common runtime pattern:
organization
→ evaluated capability set
→ application enforcement
WorkOS also highlights a practical trade-off: claims embedded in tokens become stale until session refresh and consume token/cookie space, while server-side runtime evaluation can update within a polling interval.
LaunchDarkly makes another useful distinction: entitlement targeting is typically permanent or long-lived, unlike temporary rollout flags.
These are implementation choices, but the architectural lesson is broader:
> Entitlement evaluation needs explicit freshness, lifecycle, and failure semantics.
Cache Entitlements, but Know What Staleness Means
Entitlements are read frequently and changed relatively rarely.
That makes them excellent cache candidates.
A typical request might need:
tenant entitlement snapshot
→ dozens of checks during one request
Do not query the billing provider or reconstruct plan mappings from five tables every time.
Possible layers:
database projection
→ in-process cache
→ request-scoped snapshot
But define cache semantics.
Questions you must answer
- How quickly should an upgrade become visible?
- How quickly should an immediate revocation take effect?
- Can a canceled tenant retain access for 30 seconds?
- What if cache invalidation fails?
- Does a security-sensitive entitlement require stronger freshness than a cosmetic feature?
Use TTL plus explicit invalidation where appropriate.
For highly sensitive access, consider versioning the snapshot and invalidating aggressively.
A Resolved Entitlement Snapshot Is a Powerful Read Model
Instead of evaluating every rule repeatedly, materialize a tenant snapshot:
{
"tenant_id": "tenant_42",
"version": 187,
"features": {
"analytics.advanced": true,
"api.access": true,
"branding.custom": false
},
"limits": {
"seats.max": 50,
"projects.max": 100,
"monthly_ai_credits": 10000
},
"variants": {
"support.tier": "priority"
},
"resolved_at": "2026-09-19T04:00:00Z"
}
Benefits:
- fast reads;
- consistent evaluation during one request;
- easy debugging;
- easy cache invalidation;
- clear API for other services;
- ability to compare old vs new state.
The snapshot is a projection.
The underlying subscription, plan, add-on, and override records remain the durable source material.
Explain Why an Entitlement Exists
A boolean alone is not enough for support and debugging.
Instead of:
advanced_analytics = true
your internal diagnostic view should be able to explain:
advanced_analytics = true
source = add_on
source_id = analytics_addon_2026
effective_from = Sep 1
effective_until = null
For an override:
projects.max = 250
source = tenant_override
reason = enterprise contract
expires = 2027-01-01
This does not mean sending verbose provenance on every API response.
It means the backend can answer "why?" when required.
That makes customer support dramatically easier.
Explicit Deny Can Be Valuable
Most SaaS entitlement systems are additive:
base plan grants feature
add-on grants feature
override grants feature
But enterprise and compliance cases sometimes require explicit denial.
Example:
Business plan:
ai_assistant = true
Customer compliance restriction:
ai_assistant = false
If your product needs this, define the precedence clearly.
An explicit deny often should beat inherited grants.
Do not add deny semantics preemptively if there is no real use case, because it increases complexity.
Environment Matters
Do not accidentally use production billing state to unlock staging, or test subscription data to unlock production.
Your entitlement state should have clear environment boundaries.
Possible model:
environment = production
tenant = t_42
entitlement snapshot = ...
For internal testing, use:
- test tenant;
- sandbox billing;
- explicit admin/test override;
- environment-scoped flag.
Avoid magic email addresses or hidden user IDs that bypass normal controls.
Feature Dependencies Need a Policy
Some features require others.
Example:
advanced_analytics
requires:
analytics_base
data_pipeline
You can handle dependencies in one of two ways.
Resolve them at packaging time
Only allow valid plan configurations.
Resolve them in entitlement evaluation
If Advanced Analytics is granted, automatically include prerequisites.
Prefer the simplest model that keeps invalid states impossible.
Do not force every endpoint to check a dependency graph independently.
Treat Feature Keys Like API Contracts
Once application code depends on:
analytics.advanced
that key becomes infrastructure.
Renaming it casually can break access across:
- backend services;
- frontend;
- mobile apps;
- background workers;
- API gateways;
- billing mappings;
- support tools.
Use stable lookup keys.
Stripe similarly treats feature lookup keys as stable identifiers and does not allow editing a feature's lookup key after creation.
If a capability is replaced, prefer a migration:
old key → deprecated
new key → introduced
consumers migrated
old key removed after verification
rather than silent renaming.
Billing Provider IDs Should Not Leak Everywhere
Avoid code like:
if stripe_price_id == "price_1ABC...":
allow_api_access()
That couples business logic to:
- one billing provider;
- one environment;
- one specific price object;
- one version of packaging.
Instead:
billing adapter
→ maps external subscription state
→ internal plan/package version
→ entitlement resolver
→ stable feature keys
If you migrate from Stripe to another provider, product code should not need to change everywhere.
Downgrade Safety Requires More Than Hiding Features
Suppose a customer downgrades from:
projects.max = 100
to:
projects.max = 10
but they already have 63 projects.
What happens?
Possible policy:
- keep existing 63 read-only, block creation;
- require deletion before downgrade;
- archive excess resources;
- schedule downgrade only after remediation;
- allow temporary over-limit state.
The entitlement layer says the new limit is 10.
The domain must define what an over-limit existing state means.
This is why limits are not just UI decorations.
Every quantitative entitlement needs overflow semantics.
Limit Enforcement Should Live Near the Mutation
If the entitlement says:
projects.max = 10
the strongest enforcement point is project creation.
create_project
↓
load entitlement
↓
count/lock relevant state
↓
validate limit
↓
insert
Be careful with concurrency.
Two requests can both see 9 projects and both create the 10th and 11th.
For strict limits, enforce atomically using:
- transaction-level locking;
- reservation counters;
- database constraints where representable;
- serialized mutation;
- another safe concurrency primitive.
A UI count check is not enough.
Usage Entitlements Need Period Boundaries
Monthly allowances require a clear billing or entitlement period.
Example:
monthly_api_calls = 100000
Which month?
Calendar month?
Subscription anniversary?
Billing period?
Custom enterprise period?
Define:
period_start
period_end
allowance
usage
When plan changes mid-period, define whether allowance:
- resets;
- prorates;
- increases immediately;
- changes only next period.
This belongs in product policy, not accidental implementation behavior.
Admin Overrides Need Guardrails
A support/admin panel that can alter entitlements is powerful.
Treat it accordingly.
Require:
- authenticated privileged actor;
- authorization to modify commercial access;
- reason;
- before/after state;
- timestamp;
- optional expiry;
- audit entry.
For high-risk capabilities, consider approval.
Avoid an unrestricted "edit JSON entitlements" field unless the operators truly need it.
Structured operations are safer:
grant feature
revoke feature
set limit
extend expiry
remove override
Never Silently Lose Access Because the Billing Provider Is Down
Your request path should not look like:
every API request
→ Stripe API
→ ask subscription
→ decide feature
That turns your billing provider into a synchronous availability dependency for your product.
Use local durable state.
When Stripe, Clerk, or another provider is temporarily unavailable:
- existing known entitlement state can continue;
- mutations that require fresh commercial confirmation may be restricted;
- reconciliation catches drift later.
The exact fail-open/fail-closed behavior depends on the feature.
For expensive or regulated actions, stale state may justify stronger controls.
For a low-risk cosmetic feature, brief staleness may be acceptable.
Make that decision deliberately.
Revocation Semantics Matter
Granting access is usually easy.
Revocation is where production systems get messy.
Consider:
- plan downgrade;
- payment failure;
- chargeback;
- account suspension;
- canceled add-on;
- expired trial;
- expired support override;
- manual security restriction.
Define whether revocation is:
immediate
at period end
after grace period
after resource cleanup
and whether existing long-running jobs should continue.
Example:
AI batch job started while entitlement valid
entitlement revoked mid-run
Should it complete?
For expensive workloads, the runtime may need periodic re-checks or a reservation model.
Entitlements and Background Jobs
Do not enforce entitlements only in synchronous HTTP routes.
Background systems may also create value or cost:
- scheduled reports;
- exports;
- AI agents;
- email campaigns;
- sync jobs;
- data retention processing;
- recurring integrations.
When a scheduler runs tomorrow, it should evaluate the tenant's current entitlement or operate from a durable execution grant created when the job was scheduled.
Do not assume a feature remains allowed forever because it was allowed when the schedule was created.
Entitlements and API Keys
API-first SaaS often has application/API-key access independent of human sessions.
The evaluation context may be:
api key
→ owning tenant
→ tenant entitlement snapshot
→ scope/authorization
→ API operation
A valid API key should not bypass plan limits.
Similarly, an entitled tenant should not make every API key omnipotent.
Again:
entitlement = product capability
scope/permission = actor authority
Both are required.
Entitlements and AI Features
AI makes entitlement design more important because usage has variable cost.
You might have:
ai.assistant = true
ai.premium_models = false
ai.monthly_credits = 5000
ai.concurrent_runs = 2
ai.max_run_cost = 50 credits
This combines:
- boolean capabilities;
- allowed model tier;
- monthly usage allowance;
- concurrency control;
- run-level budget.
Do not represent all of these as one "AI enabled" flag.
The entitlement system tells the runtime its commercial ceiling.
The AI usage/billing system tracks actual consumption.
This connects directly to a robust usage-based billing architecture without merging the two systems.
A Good Request-Level Evaluation API
Your application API should stay simple.
Possible semantics:
has("analytics.advanced") → boolean
limit("projects.max") → integer / unlimited
value("support.tier") → "standard" | "priority"
require("api.access") → allow or standardized denial
For quota-backed capabilities:
allowance("ai.monthly_credits") → 5000
while the usage subsystem provides current consumption.
Avoid exposing raw billing objects to every domain module.
Standardize Denial Responses
When access is denied, the system should distinguish why.
Examples:
ENTITLEMENT_REQUIRED
LIMIT_REACHED
SUBSCRIPTION_INACTIVE
TRIAL_EXPIRED
FEATURE_DISABLED
NOT_AUTHORIZED
Do not leak internal commercial details unnecessarily, but give clients enough structure to show the correct UX.
For example:
HTTP 403
code: ENTITLEMENT_REQUIRED
feature: analytics.advanced
upgrade_available: true
versus:
HTTP 403
code: NOT_AUTHORIZED
These are different user problems.
One needs an upgrade.
The other needs an administrator or permission change.
Do Not Turn Every Entitlement Check Into an Upsell
Product access controls exist for correctness.
The UI can show upgrade opportunities, but the backend should remain neutral:
allowed / denied
reason
limit state
Keeping enforcement separate from marketing copy avoids mixing core product policy with presentation.
It also helps API customers who do not have your web UI.
Observability for Entitlements
Track enough telemetry to detect broken access logic.
Useful signals include:
- entitlement-resolution failures;
- snapshot age;
- webhook synchronization lag;
- denied requests by entitlement key;
- limit-reached events;
- override creation/removal;
- customers with unknown plan mapping;
- expired overrides still active;
- subscription-to-entitlement drift;
- failed cache invalidation;
- active customers with empty entitlement sets unexpectedly.
Be careful with cardinality.
You usually need counts by feature/plan/status, not a metrics label for every tenant.
Use logs or traces for tenant-specific diagnostics.
Audit Important Entitlement Changes
An entitlement change can affect revenue, security, and customer access.
Record important mutations:
actor
tenant
source
feature
old value
new value
effective time
reason
correlation / contract reference
Examples:
- support grants Premium feature for seven days;
- sales increases seat limit;
- billing event removes add-on;
- plan migration changes API quota;
- admin revokes AI capability.
This does not require a blockchain or event-sourcing platform.
A normal append-oriented audit table is enough for many systems.
Testing Entitlements Like Core Business Logic
Pricing access bugs are production bugs.
Test the full matrix.
Plan tests
Basic has A, not B
Pro has A + B
Premium has A + B + C
Add-on tests
Pro + analytics add-on
→ advanced analytics enabled
Trial tests
active trial → paid features
expired trial → base/free features
Override tests
plan limit 10
override 50
→ resolved 50
Grandfathering tests
old Pro customer → old entitlement set
new Pro customer → new entitlement set
Downgrade tests
scheduled downgrade
→ current features remain until effective time
Failed payment tests
past_due inside grace period → access retained
past_due after grace period → downgrade/revoke
Authorization interaction
tenant entitled
user not permitted
→ denied
Concurrency tests
Two simultaneous creates should not exceed a hard quantity limit.
Cache tests
Entitlement change should invalidate or naturally expire the correct cached snapshot.
Webhook replay tests
Repeated billing events must not corrupt subscription or entitlement state.
Important Invariants
Write these as engineering invariants, not tribal knowledge.
Invariant 1
Application business logic never depends on external price IDs to decide product capability.
Invariant 2
Every protected paid feature has trusted server-side enforcement.
Invariant 3
Entitlement and authorization are both required when a feature also has actor-level permissions.
Invariant 4
Resolved tenant entitlements are reproducible from durable source state.
Invariant 5
Plan/version changes do not silently rewrite grandfathered customer access.
Invariant 6
Overrides have provenance and, when temporary, expiration.
Invariant 7
Usage-limited features separate allowance from consumed usage.
Invariant 8
Hard quantitative limits are concurrency-safe.
Invariant 9
Billing-provider outages do not erase known customer access.
Invariant 10
Subscription lifecycle events have explicit effective-time behavior.
Common Anti-Patterns
Checking plan names everywhere
if plan == "premium"
Couples pricing to implementation.
Checking only the frontend
Users can still call the API.
Using subscription active as the only gate
An active subscription does not describe which features or quantities are included.
Treating billing as authorization
Paying for a feature does not mean every user may perform every action inside it.
Treating authorization as billing
A user permission does not mean the organization purchased the capability.
Encoding every limit as a separate boolean
Quantities should usually remain quantities.
Mutating plan mappings without versioning
Breaks grandfathered customers.
Creating a new plan for every enterprise exception
Use explicit overrides or contract-specific packaging.
Calling Stripe on every request
Creates latency and availability coupling.
Using release flags as permanent paid contracts without lifecycle discipline
Temporary flags and commercial entitlements have different ownership and removal semantics.
Updating a balance without usage evidence
For quota-backed features, keep allowance and consumption explainable.
Building a policy language too early
Most SaaS products need stable feature keys, a few value types, plan mappings, and overrides—not a general-purpose rules engine.
A Practical Architecture for an Early SaaS
You can implement this without a large platform.
PostgreSQL
- tenants
- plans
- plan_versions
- features
- plan_entitlements
- subscriptions
- add_ons
- entitlement_overrides
- entitlement_snapshots
- entitlement_audit
Application
- billing adapter
- entitlement resolver
- authorization layer
- usage/quota module
Worker
- billing webhook processing
- expiry processing
- snapshot recomputation
- reconciliation
Optional caching:
in-process or distributed cache
key: tenant entitlement snapshot version
That is enough for many production SaaS products.
Do not add Kafka, a dedicated policy engine, or an entitlements microservice simply because large companies use them.
Add infrastructure when your actual workload requires it.
When a Third-Party Entitlements Product Makes Sense
Building entitlements in-house is not automatically better.
A dedicated platform or billing-provider entitlement system can be valuable when you need:
- frequent pricing/package changes;
- many plans and add-ons;
- enterprise overrides;
- sales-managed contracts;
- complex usage limits;
- real-time feature evaluation across many services;
- non-engineering teams managing packaging;
- auditability;
- fast experimentation.
Stripe Entitlements may be attractive if Stripe Billing already owns your product/subscription catalog.
Clerk can be attractive when authentication, organizations, Billing Plans, Features, and seat controls are already centralized there.
Feature-management platforms can be useful when you need sophisticated runtime targeting.
An internal implementation may be better when:
- packaging is simple;
- you already have strong subscription infrastructure;
- you need custom product semantics;
- external dependency cost/lock-in is not justified.
The architecture principles stay the same either way.
Migration From Hardcoded Plan Checks
You do not need a dangerous big-bang rewrite.
Step 1: Inventory existing checks
Find:
plan == ...
price_id == ...
subscription_tier == ...
is_premium
is_pro
across:
- backend;
- frontend;
- workers;
- mobile;
- API keys;
- scheduled jobs.
Step 2: Define stable capability keys
Map each real behavior to a feature or limit.
Step 3: Create a plan-to-entitlement mapping
Reproduce current behavior exactly before improving packaging.
Step 4: Introduce one entitlement API
Example:
has(feature)
limit(feature)
value(feature)
Step 5: Migrate server enforcement first
The backend becomes authoritative.
Step 6: Migrate UI checks
Use the same semantic capability keys.
Step 7: Add overrides and versioning only where needed
Do not redesign pricing during the technical migration unless intentionally planned.
Step 8: Remove direct plan/price checks
Add linting, code-review rules, or tests if needed to prevent them from returning.
Example: Multi-Tenant Restaurant SaaS
Consider a restaurant SaaS with three plans.
Basic
branches.max = 1
employees.max = 5
analytics.advanced = false
custom_domain = false
ai_assistant = false
Pro
branches.max = 5
employees.max = 25
analytics.advanced = true
custom_domain = true
ai_assistant = true
ai.monthly_credits = 2000
Premium
branches.max = 50
employees.max = 250
analytics.advanced = true
custom_domain = true
ai_assistant = true
ai.monthly_credits = 20000
support.tier = priority
Now a restaurant on Pro buys:
Extra Branches add-on:
branches.max += 10
and sales grants:
Enterprise pilot override:
ai.monthly_credits = 10000
expires in 60 days
Resolved:
branches.max = 15
employees.max = 25
analytics.advanced = true
custom_domain = true
ai_assistant = true
ai.monthly_credits = 10000
No endpoint needs to know:
- plan name;
- add-on SKU;
- Stripe Price ID;
- sales contract label.
When creating a branch:
check branches.max
check current branch count
create only if allowed
When running AI:
check ai_assistant
read ai.monthly_credits
read usage consumed
enforce remaining budget
When an employee opens analytics:
check tenant has analytics.advanced
check employee has analytics permission
That separation is the whole architecture.
Production Checklist
Before calling an entitlement system production-ready, verify:
- Product capabilities have stable keys.
- Plan names are not scattered through business logic.
- External billing Price IDs are isolated behind an adapter.
- B2B entitlements are tenant/account scoped by default.
- Server APIs enforce paid capabilities.
- Authorization remains separate from commercial entitlement.
- Boolean and quantitative limits are modeled appropriately.
- Add-ons compose with base plans.
- Enterprise overrides have provenance and optional expiry.
- Trials use normal entitlement semantics.
- Grandfathered packaging can be reproduced.
- Upgrade/downgrade effective times are explicit.
- Failed-payment/grace-period behavior is explicit.
- Usage allowance and usage consumption are separate.
- Hard limits are concurrency-safe.
- Entitlement snapshots have defined freshness semantics.
- Billing-provider outages do not erase known access.
- Webhook events are idempotent.
- Entitlement changes are auditable.
- Background jobs and API keys cannot bypass entitlements.
- Denial reasons distinguish entitlement from authorization.
- Tests cover plans, add-ons, overrides, trials, lifecycle, and limits.
Final Takeaway
The scalable SaaS model is not:
customer → plan name → scattered if statements
It is:
commercial state
↓
entitlement resolution
↓
stable product capabilities
↓
server-side enforcement
↓
authorization + domain rules
Billing answers what the customer bought.
Entitlements translate that purchase into product capabilities.
Authorization decides who may exercise those capabilities.
Feature flags control runtime exposure and can sometimes power entitlement evaluation, but they need the right lifecycle semantics.
Once these boundaries are explicit, plans become easier to rename, package, version, grandfather, extend with add-ons, customize for enterprise deals, and connect to usage-based pricing.
More importantly, product access stops being scattered pricing logic and becomes a real backend contract.
That is what makes an entitlement layer valuable.
Sources and Further Reading
- Stripe Billing Entitlements: https://docs.stripe.com/billing/entitlements
- Stripe API — Active Entitlements: https://docs.stripe.com/api/entitlements/active-entitlement/list
- Stripe Billing Features: https://stripe.com/billing/features
- Clerk — Features and Access Control: https://clerk.com/docs/guides/secure/features
- Clerk Billing — B2B SaaS Plans and Features: https://clerk.com/docs/guides/billing/for-b2b
- Clerk Billing — Seat-Based Plans: https://clerk.com/docs/guides/billing/seat-based-plans
- Clerk Billing — Free and Complimentary Access: https://clerk.com/docs/guides/billing/free-and-complimentary-access
- WorkOS — Feature Flags: https://workos.com/docs/feature-flags
- WorkOS — Feature Flag Runtime Integration: https://workos.com/docs/feature-flags/sdk-integration
- LaunchDarkly — Using Entitlements to Manage Customer Experience: https://launchdarkly.com/docs/guides/flags/entitlements


Discussion (0)