Greta.sh

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.

Entitlement table (worked example)
CapabilityFreeProAt limitOn lapse
Projects2Unlimitedblock + upgrade promptread-only
Team seats110blockextra seats locked
Exports5/mo (billing reset)500/mosoft warn, then blockexport allowed 30d
API accessnoyesrevoked
History30 daysfullretained 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.

Continue with Greta.sh

Explore Greta