Three catalogues sit on your application, and all three describe things a
**plan** can sell. None of them has anything to do with what a **member** is
allowed to do — that is [Authorization](/docs/concepts/authorization), and
confusing the two is the single commonest modelling mistake in a Connect
integration.

| | Feature | Limit | Add-on | Permission |
| --- | --- | --- | --- | --- |
| Granted to | a subscription | a subscription | a subscription | a member |
| Answers | "is this unlocked?" | "how many may they have?" | "what did they buy on top?" | "may *this person*?" |
| Read from | `plan.features` | `plan.limits` | `subscription.addons` | the SSO token |
| Costs money | no, it comes with the tier | no, it is the tier's ceiling | yes, on top | no |

Features and limits are the same question at two resolutions: a feature is a
boolean, a limit is a number. Reach for a limit the moment the answer is "yes,
up to five".

A customer on your Enterprise plan has every feature it bundles. Whether the
particular human logged in right now may press the button is a separate
question with a separate answer.

## Features

A feature is a capability key a plan bundles. Define it once in the
application's catalogue; each plan then picks from that list rather than
retyping a string.

```ts
if (await connect.entitlements.hasFeature(workspaceId, "campaign_builder")) {
  // ...
}
```

Two rules that save real pain:

- **The key is permanent.** Renaming one orphans every plan pointing at it.
  `name` is the field to reword; `key` is the field to leave alone.
- **Gate on the key, never on the plan.** `plan.name === "Enterprise"` breaks
  the moment you rename a tier, add a second enterprise-shaped tier, or
  grandfather somebody onto a legacy plan.

An application with no features declared has plans that can only differ on
price, which is a legitimate place to start and a bad place to stay.

## Limits

A limit is a numeric ceiling on a resource **your application owns and counts** —
sites, projects, pipelines, tracked vehicles. Declare it once in the
application's catalogue (`key`, `name`, `unit`), then give each tier its number.

```ts
const sites = await countSites(workspaceId); // yours to count

if (!(await connect.entitlements.canAdd(workspaceId, "sites", sites))) {
  throw new Error("Your plan's site limit is reached.");
}
```

`getLimit` returns the raw answer if you would rather compare yourself:

```ts
const limit = await connect.entitlements.getLimit(workspaceId, "sites");
// { model: "none" | "limited" | "unlimited", value: number | null }
```

<Callout type="warn">
**Three states, not a nullable number.** `unlimited` is a *model*, not a
`null` value — so `if (!limit.value) refuse()` reads as correct and refuses
every customer on your most generous tier. Use `canAdd`, or
`planLimitAllows` from the package root; both are the comparison Connect's own
server uses.
</Callout>

Two more things worth knowing before you model yours:

- **`plan.limits` is sparse.** A key absent from the map is `{ model: "none" }`
  — not unlimited, and not an error. `getLimit` applies that default for you;
  indexing the map by hand is what turns a missing key into `undefined` and
  then, one `?.` later, into an unbounded allowance.
- **Connect does not enforce it.** It cannot see what you are counting, so it
  stores the number and hands it over — exactly as it does for features. The
  refusal is yours to write. Lowering a tier's number never deletes anything
  either: a customer over the new ceiling keeps what they have and simply
  cannot add another.

### Selling more of a limit

A tier's number is a **base**, not the final answer. An add-on can declare that
it raises a limit, and Connect folds the two together:

```
effective = the tier's number + Σ (what each held add-on adds × how many are held)
```

So "Starter includes 1 site, buy 5-site packs" is a `sites` limit of `1` on
Starter plus a "Site pack" add-on that raises `sites` by 5. A customer holding
two packs gets `11`, and `getLimit` returns exactly that — you never join the
plan against the add-on list yourself, and somebody buying another pack
needs no change in your code.

Three rules fall out of the fold, and the first is the one to design around:

- **An unlimited tier cannot be raised.** Packs are ignored rather than added
  to a null ceiling.
- **A tier that includes none is still a floor of zero, not a veto.** A plan
  selling no sites plus two packs comes to ten, which is how you sell a
  resource purely à la carte.
- **Removing a pack lowers the ceiling and deletes nothing.** A customer over
  the new number keeps what they have and simply cannot add another — the same
  call every quota in Connect makes.

<Callout>
**A limit-raising add-on cannot also be consumable.** A consumable is a
per-cycle balance that depletes and resets; a limit is a standing ceiling that
does neither. An add-on claiming both would have its monthly grant topping up a
number being used as a ceiling, so the catalogue refuses it.
</Callout>

Limits are for a *standing* count. If going over should be **billed** rather
than refused, that is a consumable add-on, below.

## Add-ons

An add-on is a purchasable extra. The split between what lives on the
application and what lives on the plan is deliberate and is worth understanding
before you model yours.

**Behaviour is on the application:**

- `unit` — what one of them is ("messages", "GB", "seats")
- `carryForward` — whether unused units roll into the next cycle
- `overagePolicy` — `reject` blocks use past the allowance, `bill` charges for it
- `maxOverageUnits` — the hard cap on billable overage

**Price and quantity are on the plan:**

- how many are included per cycle at this tier
- what an extra one costs at this tier

<Callout>
The reason for the split: a customer must not experience "Extra API calls" as
carry-forward on Growth and forfeited on Pro. It is the same add-on, so it
behaves the same way everywhere it is offered. What a tier *includes* and what
it *charges* is exactly what should vary by tier, so that half lives on the
plan.
</Callout>

It is also what makes an add-on id keep meaning the same thing across an
upgrade: `Subscription.addons` still resolves after a plan change, which a
per-plan add-on definition could never guarantee.

### Consumable versus flat

An add-on with no consumable block is a flat recurring toggle — it is on or it
is off. One with a consumable block is a depleting per-cycle balance, and your
backend is responsible for decrementing it and for honouring `overagePolicy`
when it runs out.

`reject` means exactly that: refuse the work. If you keep serving requests past
a `reject` allowance, the customer is getting something nobody is billing for,
and the setting is decorative.

## Where each is configured

In the Console, on your application:

- **Capabilities** — the feature catalogue, the limit catalogue, and the add-on
  catalogue (identity and behaviour).
- **Plans** — which features each tier bundles, how much of each limit it
  allows, and which add-ons it offers at what price and included quantity.
- **Authorization** — permissions and roles, which are not capabilities in this
  sense at all.