
Describe what a plan makes possible
A plan can include capabilities such as advanced reports and allowances such as the number of projects a customer may create. These are its entitlements: what the customer can use, independently of the plan’s name.
Wildo resolves those declarations for the active context. Your operations and screens refer to the feature itself, so plans, add-ons and customer agreements can evolve without scattering plan-name checks through the application.
Example — Add reporting without changing every screen
A customer buys an add-on that includes advanced reports. The reporting screen and its protected operation check the same named feature; neither needs to know which combination of products granted it.
For engineers
Start with one registered meaning
This example follows Wonder Todos’ ApplicationFeature.ADVANCED_REPORTS. Its identifier and definition live in shared-lib/src/engine/features.ts; the engine shared module registers the definitions as customFeatureDefinitions. This selected definition assigns organization scope and a member-specific response:
defineFeature(ApplicationFeature.ADVANCED_REPORTS, {
scope: ResourcePrimaryScope.ORGANIZATIONS,
unavailability: {
behavior: FeatureUnavailabilityBehavior.UPGRADE_PROMPT,
byRole: { [CORE_ORG_ROLES.ORG_MEMBER]: FeatureUnavailabilityBehavior.CONTACT_ADMIN },
},
}),
The definition establishes what the feature means; it does not grant it. The enum and definition are exported through @wonder-todos/shared-lib. The builder, scope and policy enums come from @wildo-ai/saas-models.
Grant the same identifier from an offer
The existing Professional product grants advanced reports together with bulk export and custom workflows. This selected grant block belongs to ORG_PLAN_PROFESSIONAL in the application’s product.ts:
key: 'org-professional',
type: ProductType.PLAN,
targetScopes: [ResourcePrimaryScope.ORGANIZATIONS],
grantedFeatures: [
ApplicationFeature.BULK_EXPORT,
ApplicationFeature.ADVANCED_REPORTS,
ApplicationFeature.CUSTOM_WORKFLOWS,
],
The complete registered product also contains prices, plan ordering and lifecycle policy. Its catalogue is contributed through the shared module’s productDefinitions. After the subscription is synchronized, billing-derived grants enter that organization’s entitlement ledger. The product catalogue covers product authoring; the feature identifier stays the same across offers.
Require it on the operation variant
Illustrative application configuration: a normal organization-scoped report resource uses this complete READ operation entry. It reuses the real feature identifier; it is not a claim that Wonder Todos already ships this report resource. Add the entry under operations[CoreResourceOperation.READ] in the registered report resource:
import {
CORE_ORG_ROLES,
CoreResourceOperation,
ResourceOperationRiskLevel,
ResourceOperationVariantType,
type OperationConfig_ForKey,
} from '@wildo-ai/saas-models';
import { ApplicationFeature } from '@wonder-todos/shared-lib';
export const reportRead: OperationConfig_ForKey<CoreResourceOperation.READ> = {
variants: [{
variantType: ResourceOperationVariantType.API_CALL,
isDefault: true,
riskLevel: ResourceOperationRiskLevel.LOW,
roles: [CORE_ORG_ROLES.ORG_MEMBER],
requiredFeatures: [ApplicationFeature.ADVANCED_REPORTS],
}],
};
requiredFeatures belongs on the variant, alongside its access rules. The surrounding resource supplies schema, organization scope and normal READ behavior. The request must satisfy both resource authorization and the feature gate. The product does not insert this requirement for you.
The interface guide uses the same identifier with FeatureGate. The expected comparison is explicit: without an effective grant, the protected user request is refused; with the grant and ordinary read permission, it can proceed. Test the endpoint directly as well as its screen.
Choose the right kind of number
| Requirement | Declaration and behavior |
|---|---|
| A larger allowance is more generous | FeatureLimitSemantic.FLOOR: resolution keeps the most permissive allowance |
| A smaller budget is more restrictive | FeatureLimitSemantic.CEILING: resolution keeps the most restrictive finite cap |
| A resource count controls creation | Declare a custom limit with countResource; the registry installs its create check unless skipAutoRegister is set |
| Consumption is measured by a service | That service owns measurement and uses the resolved allowance; a numeric definition alone does not measure consumption |
FEATURES_UNLIMITED wins for an allowance but yields to a finite ceiling. Billing product grant aggregation and scope resolution are separate steps: do not assume every number simply adds together.
Put enforcement on the operation
Attach requiredFeatures to the protected operation variant, with explicit limitChecks where required. Boolean requirements support any-of or all-of checks; numeric checks count through the contextual repository and compare usage plus the operation’s increment with the allowance. A counting failure refuses the check.
The operation gate deliberately bypasses public execution, internal execution, repository-only/internal variants and system resources. Custom controllers can call checkRequiredFeatures; paid behavior on a bypassed path needs an explicit owning check. Frontend feature prompts explain availability; they do not secure the backend.
Resolve the effective set
Resolution layers definition defaults, the configured default profile when billing is disabled, subscription-derived grants and local manual overrides, then combines applicable scopes and prunes unmet dependencies. An override wins within its scope; a broader-scope grant can still participate in the merged context. Test both the purchased feature and its actual protected action, including the refusal case. An absent numeric limit is permissive, so registration is part of correctness.
Distinguish an absent entitlement record from a read failure
A successfully read, absent scope record uses the normal definition/profile defaults. A failed entitlement read does not: resolution fails, the protected action cannot obtain a successful entitlement decision, and no default grant is cached for the failed scope. This prevents a finite purchased ceiling from becoming unlimited during an outage, or a paid feature from being cached as unavailable.
A valid warm cache continues to serve its known result within the normal cache lifetime. After a cache-miss read recovers, the next resolution reads the ledger again. Broader-scope grants and limits still participate in normal composition; they do not turn a failed narrower-scope read into a successful decision.