Implementation worksheet · 5 min read
A Subscription Entitlement Model for a Small SaaS App
An entitlement model is one table that turns pricing-page promises into enforceable rules: each row is a capability (projects, seats, exports, API calls), each column a plan, each cell a limit, boolean, or quota-with-period — plus two columns pricing pages never show: what happens AT the limit (block, soft-warn, or meter) and what lapsed/downgraded accounts keep. Code should check entitlements ('can this account export?'), never plan names ('is this account Pro?') — the indirection is what makes plan changes, grandfathering and one-off exceptions survivable. Write the table before the pricing page, not after the refund requests.
Small SaaS teams design plans in a pricing meeting and discover at implementation that 'unlimited projects' and 'priority support' were never defined. The entitlement table is where marketing's adjectives become engineering's booleans — and disagreements surface while they're still cheap.
Put it into practice
1. List capabilities, not features
Rows are things accounts do that plans differentiate: create projects, invite members, export data, call the API, access history. If no plan differentiates it, it's not an entitlement — keep the table small enough to enforce.
2. Fill cells with one of three types
Boolean (can/cannot), limit (max 3 projects), or quota-with-period (500 exports/month, resets on billing date). Name the reset rule in the cell — 'monthly' quotas that reset on calendar vs billing day is a classic silent dispute.
3. Add the at-the-limit column
For every limit: hard block with upgrade prompt, soft warn with grace, or metered overage. This column IS your upgrade funnel — a hard block on the capability users love is pressure; on the capability they need mid-task, it's a support ticket.
4. Add the lapse column
What does a downgraded or expired account keep? Read-only access, export rights for 30 days, data retained for 90? This column prevents both accidental data hostage-taking and accidentally free service.
5. Enforce entitlements, never plan names
One function answers 'can account X do Y?'; the pricing page and the code both read the same table. Grandfathered accounts get their own column — legacy-Pro keeps old limits without an if-ladder in every handler.
Entitlement table (worked example)
Copy this structure into your review document and record your observed result for each row.
| Capability | Free | Pro | At limit | On lapse |
|---|---|---|---|---|
| Projects | 2 | Unlimited | block + upgrade prompt | read-only |
| Team seats | 1 | 10 | block | extra seats locked |
| Exports | 5/mo (billing reset) | 500/mo | soft warn, then block | export allowed 30d |
| API access | no | yes | — | revoked |
| History | 30 days | full | — | retained 90d |
A failure worth checking
The plan-name trap: code full of `if (plan === 'pro')` checks. The day marketing renames Pro to Growth, launches a promo tier, or grandfathers old pricing, every check is a bug site. Entitlement indirection costs one lookup function and buys every future pricing change; teams skip it exactly once.
Common questions
Should entitlements live in my billing provider or my database?
Your database, synced from billing webhooks. The billing provider knows what was paid; only your app knows what that means. Deriving entitlements at request-time from a Stripe API call adds their uptime to yours on every page load.
How do I handle one-off exceptions for big customers?
An overrides layer on top of the table — account-level entitlement patches with an expiry date and a reason field. Undocumented forever-exceptions become invisible pricing plans with one customer each.
Basis and scope
This is a proposed implementation method using illustrative examples, not a measured benchmark or a customer case study. Prepared with AI assistance. Validate product-specific behavior against current documentation and your own test environment.