License, Quota, and Policy
Three checks decide whether a tenant registration is admitted. The license must allow it. The quota must be available. The onboarding policy must accept the caller. All three run before any change is made. Each has a documented default, and each is an extension point a deployment can replace.
License
A license carries a signed set of claims: an identity, the parties it binds (issuer, licensee, installation owner), a validity window, the installation binding, the grant itself, the policies it asserts, and revocation material. The grant is the entitlement authority. It carries the licensed products, the resolved feature set, command-level overrides, and the quotas. The runtime uses this signed grant directly. The installed license service parses the signed envelope the deployment uses, and the rest of the EDK consumes the resolved snapshot.
The license service reads a signed envelope as its runtime source. On JVM deployments it reads a compact license envelope, decrypts it with the configured recipient private key, validates the embedded signature against the mounted root CA bundle, and checks revocation material when a CRL bundle is configured. The EDK consumes the parsed snapshot. The service that produces it runs the cryptographic checks before any tenant or entitlement decision sees the license.
License runtime failures are fail-closed. A missing source reports MISSING. An invalid, malformed, untrusted, or clock-unsafe source reports a blocked status. An expired license reports EXPIRED. Entitlement checks only pass for ACTIVE, GRACE, or RECOVERY snapshots. Recovery mode can reuse a last-good snapshot when the current source cannot be read, but only before the original signed expiry and only within the configured recovery window. Expiry grace is also bounded by the signed license. license.runtime.expiry-grace-cap-days is a local maximum. It is not a way to extend a license beyond the grace period granted in the signed claims.
License material
A server or container deployment requires a license that is both signed and encrypted. The license token is a JWS whose signing certificate chains to the Sphereon licensing root CA, wrapped in a JWE addressed to the deployment's recipient key. The runtime decrypts the envelope with the configured recipient private key, validates the signing chain against the mounted root CA bundle, checks revocation material when a CRL bundle is configured, and then parses the claims.
The license source resolves in a fixed order. An operator-activated license installed through the admin API wins, then license.path, then license.token, then the development license behind the explicit license.dev.enabled opt-in. The license.* keys:
| Key | Purpose |
|---|---|
license.path | File path of the license envelope. |
license.token | Inline license token, used when no file path is set. |
license.recipient.key-id | Identifier of the recipient key the license is encrypted to. |
license.recipient.private-key-path | File path of the recipient private key that decrypts the envelope. |
license.trust.root-ca-bundle-path | File path of the Sphereon licensing root CA bundle. Required: without a bundle, every license is denied. |
license.dev.enabled / license.dev.path | Explicit opt-in for a development license file as a last-resort source. A development license is an alternative source only. It passes the same trust-chain validation and may be valid for at most 31 days. |
license.runtime.recovery-days | How long a last-good snapshot may stand in when the current source cannot be read, never beyond the original signed expiry. |
license.runtime.expiry-grace-cap-days | Local cap on the post-expiry grace window, bounded by the grace granted in the signed claims. |
license.runtime.last-good-snapshot-path | Where the last successfully verified snapshot is persisted for recovery mode. |
license.runtime.last-seen-time-path | Where the last observed wall-clock time is persisted for clock-rollback detection. |
license.runtime.clock-rollback-tolerance | How far the clock may move backwards before the license is reported clock-unsafe and blocked. |
Trust model
License signing certificates chain to the Sphereon licensing root CA, either through Sphereon intermediates or through partner intermediates. Partner intermediates carry constrained certificate policy OIDs, so a partner CA can sign licenses only for the products its policy allows. The system trust store is never consulted. The only trust anchor is the configured root bundle.
Four keys tune the chain checks. Each accepts a comma-separated OID list, and setting a key to an empty value disables that individual check. Disabling a check never removes the requirement for the root bundle itself.
| Key | Check |
|---|---|
license.trust.license-signing-eku | Extended key usage the leaf signing certificate must carry. Defaults to the Sphereon license-signing EKU. |
license.trust.required-policy-oids | Certificate policies the leaf must assert. Defaults to the Sphereon license-signing policy. |
license.trust.partner-ca-policy-oid | Policy OID that marks an intermediate as a partner CA. Defaults to the Sphereon partner-CA policy. |
license.trust.partner-allowed-policy-oids | Product policies a partner CA may sign for. Defaults to the Sphereon product policies. |
Tenant quotas
The tenant subsystem reads cardinality quotas from the license to cap how much tenant structure a deployment may stand up:
| Quota | What it caps |
|---|---|
| Root-tenant count | The number of customer (non-system) root tenants. |
| Total-tenant count | The total number of customer tenants across the tree. |
| Hierarchy depth | How deep the tenant tree can go (a root is depth 1, its child depth 2). |
Each is a cardinality quota. The runtime reads the live count and refuses a registration that would exceed the cap. Whether a tenant may have a parent at all is governed by the subtenants feature below.
Onboarding features
Features are the coarse capability gates the onboarding policy reads against a registration. The licensing-relevant ones for tenant onboarding:
| Feature | What it gates |
|---|---|
subtenants | Whether a tenant may have a parent. Without it, every registration with a parent is refused. This is useful for root-only editions. |
custom-domains | Whether custom-domain records are allowed. Without it, only platform subdomains. |
self-signup | Whether the public signup endpoints accept requests. Without it, they report unavailable. |
federation | Whether external-IdP owners are accepted. Without it, only local owners. |
Features are namespaced, versioned keys carried in the signed grant, and the catalog is extensible. An enterprise or VDX deployment contributes its own features and the policy reads them the same way. Features explains how features are declared and enabled. The Catalog covers what the deployment exposes.
The default posture
Without a license service installed, a bare or development build runs with no entitlement ceiling, so the standard surface works for local development. A production deployment installs a license service that reads the actual signed license and resolves the entitlements and quotas the customer holds. See Application Tenant and Bootstrap for installing and verifying a license.
Command-level enforcement and audit
Entitlement checks run per command. A service command's contract can carry a license-protection descriptor. Its entitlement key has exactly four segments, {product}.{module}.{service}.{command}, and its protection mode decides what enforcement does. NONE, INTERNAL_SYSTEM, and DEVELOPMENT_ONLY commands execute without consulting the license. A LICENSED command requires its entitlement key to be granted by an ACTIVE, GRACE, or RECOVERY license snapshot. A denial surfaces to the caller as a forbidden error carrying a machine-readable reason plus safe license identifiers only, never the raw license payload or key material.
The descriptor also carries the inputs the runtime needs to meter and gate the command:
featureKeys: the coarse feature gates the active license must grant for the command to run. A command with feature keys is allowed when every key resolves to a truthy value in the effective entitlement set.costWeight: the relative weight a single invocation consumes against any metered quota.0is exempt and is never tracked.1is the baseline. A heavier operation declares a higher weight so one call draws down more of a metered window.quotaKeys: the quotas a successful invocation counts against (see Quota below).deploymentShapePredicates: the deployment shapes (for examplepackaging=container,environment=production) where enforcement applies, so a command can be licensed in production yet unrestricted in development.
The denial model
Every entitlement decision resolves to a single canonical machine-readable reason, so callers, audit reports, and the admin console all speak the same vocabulary. The reasons fall into two families. Decision reasons describe why an installed, valid license did not entitle the command. Envelope reasons describe a missing or unusable license.
| Reason | Meaning |
|---|---|
UNKNOWN_FEATURE_KEY | The command requires a feature the catalog does not list. |
NOT_ENTITLED | The effective entitlement set does not grant the required feature or command. |
QUOTA_EXCEEDED | A metered quota window is exhausted, or a cardinality cap is reached, for this scope. |
CEILING_EXCEEDED | The request exceeds what the signed license itself permits, a limit that activation or a subscription cannot raise. |
COMMAND_DENIED | A command-override deny rule matched. A deny always wins. |
PARTY_RESOLUTION_FAILED | The issuer, licensee, or installation-owner party could not be resolved. |
MISSING_CONTRACT | The command has no registered contract. |
MISSING_DESCRIPTOR | The contract carries no license descriptor. |
MALFORMED_DESCRIPTOR | The license descriptor failed structural validation. |
LICENSE_MISSING | No license is installed for this installation. |
LICENSE_EXPIRED | The installed license is past expiry, beyond any grace. |
LICENSE_INVALID | The installed license is structurally or cryptographically invalid. |
The decision is deterministic and fail-closed. Deny rules are evaluated before allow rules, an unavailable ceiling denies everything, and a command whose declared feature is unknown to the catalog is denied rather than waved through. Entitlement Resolution walks the full decide flow end to end.
Two keys tune the enforcer. license.command-enforcement.enabled (default true) switches per-command enforcement on or off as a whole. license.command-enforcement.missing-descriptor-mode decides what happens when a command has no registered contract or no license descriptor. The default warn logs the gap and lets the command run. deny fails closed so an uncovered command cannot execute. Warn mode is for rolling coverage out across a large command inventory. A hardened deployment runs deny.
Coverage is observable before it is enforced. In warn mode, the runtime inspects the command inventory and reports commands with no registered contract, contracts without a license descriptor, malformed descriptors (including entitlement keys that are not four segments), and entitlement keys claimed by more than one contract. Commands that are intentionally outside licensing, such as health checks, go on an allowlist with a recorded reason, per command id or per id prefix. Individual command-id prefixes can be flipped to hard-fail so a coverage regression in a protected module breaks loudly rather than warning.
Every denial is also an audit event. The enforcer emits exactly one license.command.denied event per denial, with a policy-denied result, the denial reason as the error code, and metadata limited to the entitlement key, license id, deployment id, and license status. In warn mode, a command missing its descriptor emits a single license.command.descriptor-missing warning event the first time it is seen, so the gap is visible in the audit trail without flooding it. Descriptor defects carry their own reason codes (MISSING_CONTRACT, MISSING_DESCRIPTOR, MALFORMED_DESCRIPTOR). An audit failure never blocks the enforcement decision itself. The denial still reaches the caller.
Quota
A quota is a named cap the license grants, identified by a quota key and carrying a hard limit. Quotas come in two kinds. The kind decides how the runtime measures consumption.
- Cardinality quotas count live state. The limit caps how many of a thing may exist at once: root tenants, total tenants, protocol-surface instances. When a command that creates such a thing runs, the runtime reads the current live count and refuses the create when it is already at the cap. Deleting the thing frees the slot again, so the count tracks reality rather than a running tally. Two creates racing for the last available slot are serialised at the database level, and the loser is denied.
- Metered quotas count usage within a window. The limit caps how many units may be consumed within a rolling window (for example one month), and consumption accrues rather than freeing up. The window resets, not individual operations. Each invocation draws down the command's
costWeightunits from the window. A command that fails never leaves usage charged against the window. If the window cannot fund an invocation, the command is denied withQUOTA_EXCEEDEDand never executes.
A quota's consumeOn setting decides when the charge sticks. The default, SUCCESS, only charges on a successful invocation. ATTEMPT charges even when the invocation fails, which is appropriate for rate-limiting expensive work whose cost is incurred regardless of outcome. Quota enforcement is fail-closed. A missing provider for a required cardinality quota, or a counter-store error, denies rather than silently allowing.
Quotas apply at two scopes at once. The same charge is checked against the platform-wide bucket and the per-tenant bucket, so neither a single tenant nor the deployment as a whole can exceed its allowance. The current limit and usage for every quota key are visible to operators as a read-only projection. See the capacity meters described in Application Tenant and Bootstrap.
Onboarding policy
The onboarding policy is the authorization chokepoint for tenant registration. It runs first, before any database query or slug-uniqueness check, so an unauthorized caller cannot probe the registration endpoint to enumerate slugs. Returning a rejection short-circuits the whole registration. Nothing is created and nothing leaks about the deployment's existing tenant inventory.
The policy decides per registration source:
- Operator registration (an authenticated admin call). The policy checks the acting tenant and roles. Is this the application tenant (platform admin can act on any tenant)? Is it the parent of the subtenant being created (a parent admin acts on its children)? Are the roles sufficient? Are the licensed features and tenant quotas satisfied?
- Self-service signup (the materialisation of a confirmed signup request). The policy validates that the request was confirmed, that the email, slug, and parent match the request, that any required approval was granted, and that the relevant license features are present.
- Bootstrap (the one-shot platform tenant claim). Accepted unconditionally, because the durable bootstrap gate has already proved the deployment is in its first-run state.
The fail-closed default and the extension point
By default the onboarding policy fails closed. It rejects every operator and self-service registration and accepts only the durable-gated bootstrap. A deployment must provide an onboarding policy that decides who may register or sign up. Without one, only the very first bootstrap tenant can be created. The replacement interprets roles, parent-tenant scope, license features, and signup state. Installing the production onboarding module automatically swaps in its policy.
Signup protection
Self-service signup has its own layer of protection on top of the onboarding policy, because its lifecycle (request, resend, confirm, approve, reject) is richer than a single registration check.
- Signup policy. Decides whether root and child signup are enabled, whether they require operator approval, the rate limits per email and per IP, and the verification-token TTL. The default reads the
tenant.signup.platform.*andtenant.signup.children.*configuration keys. A deployment can replace it to read, for example, per-parent-tenant approval overrides. - Challenge verification. A bot challenge sits in front of the public signup endpoint. To enable public signup, provide a challenge verifier integrating with reCAPTCHA, Cloudflare Turnstile, hCaptcha, or your standard. The EDK ships no default. With no verifier installed, every public signup request is rejected.
- Approval authorization. Decides who can approve or reject pending signups. The default lets platform admins act on any pending request and parent-tenant admins act on requests scoped to their tenant.
- Token hashing. Signup verification tokens are stored hashed. The plaintext goes to the email service at issue time and is then forgotten by the EDK. The default is SHA-256 with constant-time comparison. A deployment can substitute a different hash if its security posture requires it.
Extension points
To customise a behaviour, provide the matching component. The default does what the right column says.
| To customise | Provide | The default |
|---|---|---|
| Quota and entitlement enforcement | A license service that reads your signed license | No ceiling: the standard surface for local development |
| Who may register or sign up | An onboarding policy | Fails closed: only bootstrap is accepted |
| Whether the entitlement resolver decides | An entitlement resolver | Fails closed: nothing is entitled until a resolver is installed |
| Bot protection on public signup | A signup challenge verifier | Fails closed: no verifier rejects every public signup |
| Signup enablement, approval, rate limits, TTL | A signup policy | Reads the tenant.signup.* config keys |
| Who can approve/reject signups | A signup approval policy | Platform admin and parent-tenant admin |
| Signup token hashing | A token hasher | SHA-256, constant-time |
The defaults are chosen so a bare EDK build either works correctly (quota, token hashing) or fails closed (onboarding policy, challenge verifier). A production deployment explicitly provides the parts it wants to enable.