How Versus packages its functional modules into licensed entitlements and curated bundles for hosted tenants — what Platform Core always includes, how the per-tenant entitlement is resolved and enforced, and how an operator sets one from the Control Plane.
Versus separates what the platform can do from what a given customer has bought. The platform ships as one application, but its functional areas are packaged as modules, and each hosted (SaaS) tenant carries an entitlement that says which modules it is licensed for. This page explains the module catalog, the curated bundles that sit on top of it, and how entitlements are resolved and enforced across the platform.
This is the hosted side of licensing. On-premise installs use a signed license file instead — see the note under How enforcement works — but the module catalog and the enforcement gate are shared between the two models.
Every Versus deployment includes Platform Core — the parts of the product that are always available and are never gated:
On top of Platform Core sit the licensable modules. These are the functional areas that a tenant is granted individually:
| Module key | Module |
|---|---|
tls_clm | TLS / CLM (certificate lifecycle) |
firewall | Firewall |
secure_vault | Secure Vault |
adc | ADC (application delivery) |
ipam_ddi | IPAM / DDI |
data_warehouse | Data Warehouse (Snowflake) |
The module key (the left column) is the stable identifier used everywhere — in the API, in the tenant's entitlement record, and in the enforcement gate. Platform Core is never stored on an entitlement; it is simply always on.
Rather than pick modules one at a time, most tenants are sold a bundle — a named preset that expands to a fixed set of modules. Versus ships four:
| Bundle | Modules included |
|---|---|
| Keystone | tls_clm, secure_vault |
| Perimeter | firewall, tls_clm, ipam_ddi |
| Fabric | adc, ipam_ddi, tls_clm |
| Continuum | adc, firewall, tls_clm, ipam_ddi, secure_vault |
A bundle is a convenience, not a container. When you assign a bundle, its module list is expanded and stored on the entitlement; the bundle name is kept alongside for display and reporting. You can still add or remove individual modules afterward — an explicit module list always wins over the bundle preset — so a tenant can be sold "Perimeter plus the Data Warehouse module" without a new bundle.
Each hosted tenant has at most one entitlement — a server-authoritative record attached one-to-one to the tenant. There is no signed file on the SaaS path; the database is the source of truth. An entitlement carries:
| Field | Meaning |
|---|---|
plan | trial, subscription, or nfr (not-for-resale) |
bundle | The bundle name (Keystone … Continuum) or empty for à-la-carte |
modules | The licensed module keys, e.g. ["tls_clm","firewall"] |
limits | Optional numeric caps, e.g. {"max_devices": 500} |
expires_at | When the term ends (null = no expiry) |
status | active, suspended, or expired |
An entitlement is effective only when its status is active and it has not passed expires_at. A suspended or expired entitlement is not effective, and the platform treats a non-effective entitlement the same way it treats a tenant with no entitlement at all.
The entitlement system is additive and inert by default. A tenant with no entitlement row — or one whose entitlement is not effective — resolves to all modules. This is deliberate: it means existing and demo tenants keep working with everything unlocked, and shipping the licensing machinery changes nothing until a tenant is explicitly given an entitlement and enforcement is switched on.
Enforcement is controlled by a single global switch, the ENTITLEMENTS_ENFORCED setting (driven by an environment variable). When it is off — the default — every module resolves as available for every tenant, regardless of what any entitlement says. When it is on, a tenant's effective module set becomes the boundary.
Enforcement runs at two layers:
Navigation gate (front end). The single-page app fetches the caller's entitlement once and hides workspaces the tenant isn't licensed for. This gate fails open: until the entitlement loads, or if the call errors, nothing is hidden. It is a usability layer, not a security boundary.
API gate (back end). A middleware sits in front of the API and is the real boundary. It maps each request path to the module that owns it; requests that don't map to a gated module are Platform Core and always pass. For a gated path, it reads the caller's tenant from the verified access token, resolves that tenant's effective modules, and either lets the request through or denies it.
A denial is an HTTP 402 Payment Required with a small JSON body naming the module and flagging that an upgrade is needed:
{
"error": "This feature is not included in your plan. Contact your account team to add it.",
"module": "adc",
"upgrade": true
}
The middleware trusts only the token: it reads the tenant_id claim from the verified JWT itself rather than relying on view-level auth, so a request can be gated before it reaches the view. A forged token doesn't help an attacker — the view still rejects it — and a valid token always carries the caller's real tenant.
The API middleware handles both deployment models behind one class. On a hosted deployment it consults the per-tenant entitlement described here. On an on-premise install it instead consults the applied, cryptographically signed .veslic license, gating the same module paths by the features that license grants (and flipping the install read-only after a license lapses past its grace period). The module catalog and the path-to-module map are shared, so a module means the same thing in both worlds. See Platform Architecture for how the deployment models relate.
The API gate keys off request path prefixes. A representative sample of the mapping:
| API path prefix | Required module |
|---|---|
/api/clm/, /api/certs/ | tls_clm |
/api/firewall-automation/ | firewall |
/api/vault/, /api/psm/, /api/extvault/ | secure_vault |
/api/devices, /api/virtual-servers, /api/pools, /api/irules, /api/waf- | adc |
/api/ipam- | ipam_ddi |
/api/warehouse/ | data_warehouse |
Note that related features ride an existing module rather than needing their own: Privileged Session Management (/api/psm/) and external vault credentials (/api/extvault/) are both gated by secure_vault. Any path not in the map is Platform Core and is never gated.
Any authenticated user can ask the platform what their tenant is licensed for. This is the same call the SPA uses to build its navigation gate:
GET /api/tenants/my-entitlement/
Authorization: Bearer eyJhbGc…
{
"enforced": true,
"modules": ["firewall", "ipam_ddi", "tls_clm"],
"bundle": "Perimeter",
"plan": "subscription",
"status": "active",
"expires_at": "2027-01-31T00:00:00Z",
"limits": {}
}
The modules array is the tenant's effective set — what it can actually use right now, already accounting for the enforcement switch. When enforcement is off, or when the tenant has no effective entitlement, this returns the full module list, which is why the front end never hides anything by default.
Entitlements are administered from the Control Plane, the operator console for managing tenants. Two flows write them.
When you create a production tenant, you set its entitlement in the same step. The provisioning call takes the organization details plus either a bundle name or an explicit module list:
POST /api/control-plane/tenants/provision/
Authorization: Bearer eyJhbGc…
Content-Type: application/json
{
"org_name": "Northwind Bank",
"org_code": "northwind",
"admin_email": "[email protected]",
"plan": "subscription",
"bundle": "Perimeter",
"years": 1
}
Versus provisions the tenant (identity org, admin, and invite) and then stamps its entitlement in one transaction. Supply bundle to use a preset, or modules with an explicit list of keys to go à la carte; if you pass both, the explicit list wins. The term is set from years or days, and unknown module keys are filtered out.
To change a tenant already in service, post the fields you want to update to its entitlement action:
POST /api/control-plane/tenants/<tenant_id>/entitlement/
Authorization: Bearer eyJhbGc…
Content-Type: application/json
{
"bundle": "Continuum",
"status": "active",
"years": 2,
"note": "Upgraded from Perimeter at renewal"
}
The same endpoint answers GET with the current entitlement plus its recent change history. Every write records an audit entry — who changed it, when, and the before/after state — so the entitlement of a tenant is fully traceable over its life. Use status to suspend (suspended) or expire an account without deleting its entitlement; because a non-effective entitlement resolves to no gated modules under enforcement, suspending a tenant cleanly withdraws access to everything beyond Platform Core.
Entitlements are the licensing layer, not an access-control layer. They decide which modules a tenant has bought; they do not decide which users inside that tenant may use them — that is the job of roles and permissions. The two compose: a user sees a workspace only if their tenant is licensed for the module and their role permits the action.
Because the module catalog is shared with the on-prem signed-license path, and because Platform Core is always present, the same product runs unchanged whether a customer is on a hosted plan, an on-prem license, or a full-access demo. The only difference is the entitlement — a small, auditable record that the platform resolves on every gated request.
secure_vault.