
Make access decisions inspectable
Entitlement state is stored as a resource, not hidden only in a billing provider or a browser flag. The application, an organisation and an individual each have a defined place for their granted features, allowances and manual exceptions.
That gives administration, billing synchronisation and feature resolution a common record to work from, while preserving who is allowed to change it.
Example — Explain why a feature is available
An operator sees reporting in the plan-derived set and a higher project allowance in the manual override block. The two sources explain the customer’s package without reverse-engineering a collection of plan-name conditions.
For engineers
The resource contains a grant source plus a separate override object. The resolver combines those with defaults, applicable profiles and other scopes. A stored row therefore does not by itself describe every feature available in a request.
Application example: an authorized administrative view shows the stored organisation agreement beside its effective scope result. The context and organisation identifier must refer to the same authorized customer. Keep the external context when using the resource service so its read policy remains in force:
import {
CoreResourceType, ResourcePrimaryScope,
type ScopeFeaturesWithOverrides,
} from '@wildo-ai/saas-models';
import type {
ExecutionContext, FeatureResolutionBackendService,
ServicesRegistryHandlerBackendService,
} from '@wildo-ai/saas-backend-lib';
export async function inspectOrganizationAccess(
services: ServicesRegistryHandlerBackendService,
featureResolution: FeatureResolutionBackendService,
context: ExecutionContext<any>,
organizationId: string,
) {
const stored = await services.read<ScopeFeaturesWithOverrides>(
CoreResourceType.ORGANIZATION_FEATURES, context, { organizationId },
);
const effective = await featureResolution.resolveForScope(
ResourcePrimaryScope.ORGANIZATIONS, organizationId,
);
return {
stored,
effective,
sourceRecordExists: stored !== null,
};
}
The explicit filter selects the owner’s record; it is not a replacement for authorization. This helper neither grants access nor refreshes caches. The resolver may return its cached scope result. After an administrative change, distinguish a fresh resolution from a previously cached result.
Illustrative before/after, assuming reporting has no default, profile or other grant:
| Point in the agreement | Stored base features | Manual addedFeatures | Fresh effective reporting |
|---|---|---|---|
| Before the exception | Does not include reporting | Does not include reporting | Disabled |
| Operator grants reporting | Unchanged | Includes reporting | Enabled |
| Billing refreshes the plan | Recomputed from the subscription | Reporting exception preserved | Enabled while the exception applies |
| Operator removes the exception | Whatever the current plan grants | Reporting exception removed | Determined by the remaining sources |
removedFeatures and limitOverrides live beside addedFeatures in manualOverrides. An absent row is a legitimate result and can still resolve defaults or an applicable profile; a failed read is an error, not an empty agreement. Neither the ledger nor this inspection helper contains an automatic expiration schedule for exceptions.
The effective result above belongs to one organisation. resolve(context) answers the different question of effective access across the request’s applicable application, organisation and user layers. Use the appropriate result when explaining a protected action.
Use the resource that owns the agreement
| Resource | Ownership and read policy | Write policy |
|---|---|---|
APPLICATION_FEATURES | Application-wide, super-administrator reads | Super-administrator |
ORGANIZATION_FEATURES | Organisation-context member reads | Super-administrator |
USER_FEATURES | User-context resource with its configured member read role | Super-administrator |
Resource relationships determine the contextual address. Use generated resource operations and their actual context rather than inventing an unscoped /features mutation. Generic updates are powerful administration operations because the override fields must remain writable to the operator; tenant users cannot use that authority.
Preserve both producers
Billing sync owns the subscription-derived features and limits. Named enable/disable/reset operations own their specific override mutations and invalidate the affected scope cache. A bespoke administrator that edits records directly must also handle cache freshness through an authorised backend flow.
For verification, read the record after a subscription change, check that its manual exceptions remain, then inspect the resolver’s effective result and exercise a protected action. A successful database write alone proves neither fresh resolution nor backend enforcement. Keep direct storage repair separate from normal administrative operations so it cannot silently bypass these effects.