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.