# Plans and Entitlements — Multi-Tenant SaaS Architecture

Source: https://www.skillbyai.com/en/multi-tenant-saas/mt-entitlements

> Model plans, per-tenant overrides and limits as data, and check entitlements in one place before allowing a feature or a new seat.

## What a tenant is allowed to do

An **entitlement** is a right a tenant has bought or been granted: a feature (SSO, API access, audit logs), a limit (25 seats, 100 GB storage) or a level of service. Plans bundle entitlements, but real businesses also need **overrides**: a sales deal that adds API access to a smaller plan, a beta flag for a few tenants, a temporary limit raise. Model all of this as data owned by the control plane, and expose one function or service that answers "is tenant X entitled to Y?". Feature code calls that check instead of comparing plan names, because `if plan == 'pro'` scattered across the code breaks the day marketing renames a plan or adds a new one. Entitlements differ from **feature flags** used for gradual rollouts: a flag says whether code is switched on yet, an entitlement says whether this customer has paid for it. Many systems evaluate both.

## From plan to permitted action

A tenant's plan supplies default entitlements, overrides adjust them, the entitlement check combines both, and the feature is allowed or denied.

![Four stages: tenant plan, per-tenant overrides, central entitlement check, allow or deny decision.](assets/figures/multi-tenant-saas/section-8-map.svg) — Figure 8.1: plan defaults -> tenant overrides -> entitlement check -> allow or deny

## One place to ask

Plans and overrides are plain data. The free tenant zen gets API access through an override but still cannot use SSO.

```python
PLANS = {
    "free": {"seats": 3,  "features": {"projects"}},
    "pro":  {"seats": 25, "features": {"projects", "sso", "api"}},
}
# Per-tenant overrides (a sales deal, a beta) win over the plan default
OVERRIDES = {"zen": {"features": {"api"}}}

def entitled(tenant, plan, feature):
    extra = OVERRIDES.get(tenant, {}).get("features", set())
    return feature in PLANS[plan]["features"] | extra

def can_add_seat(plan, used):
    return used < PLANS[plan]["seats"]

print("zen api: ", entitled("zen", "free", "api"))
print("zen sso: ", entitled("zen", "free", "sso"))
print("acme sso:", entitled("acme", "pro", "sso"))
print("zen 4th seat:", can_add_seat("free", 3))
```

Output:

```
zen api:  True
zen sso:  False
acme sso: True
zen 4th seat: False
```

## Enforce limits on the server

Hiding the "add user" button when the seat limit is reached is good UX, not enforcement. The API that creates the membership must run the same entitlement check, or a direct API call bypasses the limit.

**Quiz:** Why is `if tenant.plan == 'pro':` in feature code a poor way to gate features?

- [ ] Python cannot compare strings
- [ ] Plans should never have names
- [x] It ignores overrides and breaks when plans are renamed or added; a central entitlement check avoids both
- [ ] It is slower than a database query

*Answer:* It ignores overrides and breaks when plans are renamed or added; a central entitlement check avoids both. Plan-name checks hard-code commercial packaging into product code. A central entitlement check handles overrides and new plans without hunting through the codebase.
