Capabilities — features, limits and add-ons
What a plan unlocks, how much of it a tier allows, what a customer can buy on top, and why behaviour lives on the application while price lives on the plan.
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, 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.
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.
nameis the field to reword;keyis 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.
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:
const limit = await connect.entitlements.getLimit(workspaceId, "sites");
// { model: "none" | "limited" | "unlimited", value: number | null }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.
Two more things worth knowing before you model yours:
plan.limitsis sparse. A key absent from the map is{ model: "none" }— not unlimited, and not an error.getLimitapplies that default for you; indexing the map by hand is what turns a missing key intoundefinedand 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.
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.
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 cycleoveragePolicy—rejectblocks use past the allowance,billcharges for itmaxOverageUnits— 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
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.
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.
Plans, pricing and free tiers
What a plan is in Connect, why a monthly and an annual price are two rows, and what happens when a customer changes tier or cadence.
Authorization — permissions and roles
What your application declares, what Connect resolves, and why Connect never enforces your permissions for you.