Plans & tiersCapability gating

Capability gating

Tiers decide what a box can do. Enforcement happens in two places, both ahead of runtime rather than through a per-request entitlement check.

Two enforcement points

1. Engine — composition-time gating

The engine only imports packages that were installed, and only entitled packages can be installed (the gated registry refuses the rest). So a basic box simply does not contain a growth-only module — its routes never exist. There is no runtime tier check on the hot path; the absence of the package is the gate.

2. Console — install-time capability set

box activate writes the box’s capability set into the console’s environment at install time. The dashboard reads it to decide which surfaces to render, so staff never see a button that 404s because the backing engine package isn’t present.

There is intentionally no runtime /capabilities endpoint in v1. Gating is resolved at install time and baked into both the engine (by composition) and the console (by env), which keeps request handling free of entitlement lookups.

Why the model changed

Earlier, page groups were too coarse — a whole group shipped if any trigger engine was entitled — and the frontend had no tier awareness, so every surface rendered and clicking one for a missing engine 404’d. Capability gating fixed both: the entitled set is computed precisely (dependency closure) and the console is told exactly what it may show.

Tier placement decisions

Some capabilities were deliberately assigned:

  • Basic-tier surfaces: analytics, invoices, customers (list/CRUD), reviews, bookings.
  • Growth and up: subscriptions, loyalty, blog, customer-segment, campaigns, feedback, inquiry, waitlist, venue, location.

The net-new engines that closure analysis pulled into basic were analytics, invoice, review, booking, and calendar-sync.

⚠️

Placement is defined by tier-manifest.ts and may evolve. The activation response’s packages array is always the source of truth for a given license.