
Keep customer agreements separate from the plan
An organisation’s entitlement record separates what its subscription grants from the exceptions an application operator makes for that customer. An upgrade can refresh the plan-derived values without erasing the agreement.
Customers can inspect their entitlement state through the scoped resource. Changing it is an application-administrator responsibility, so buying access and granting access remain distinct actions.
Example — Include one feature in a customer agreement
A customer stays on its current plan and receives reporting as a negotiated exception. The operator records an added feature for that organisation; a later subscription update preserves the exception.
For engineers
The organisation has a scoped ORGANIZATION_FEATURES record. Billing synchronisation writes its features and limits; discretionary changes belong in manualOverrides. Keep those sources separate instead of editing a computed plan grant as if it were permanent.
The actual request contracts for the named administrator actions are:
export const OrganizationFeaturesEnableFeatureRequestDtoSchema = z.object({
featureId: z.enum(CoreFeature),
reason: z.string().max(500).optional()
});
export const OrganizationFeaturesDisableFeatureRequestDtoSchema = z.object({
featureId: z.enum(CoreFeature),
reason: z.string().min(1).max(500)
});
export const OrganizationFeaturesResetFeaturesRequestDtoSchema = z.object({
reason: z.string().min(1).max(500)
});
These selected exports show that the enable/disable verbs accept CoreFeature identifiers. They are not a generic custom-feature endpoint. For an application-defined feature or a numeric exception, use the authorised resource update contract to write the appropriate override fields, through the normal resource operation pipeline. Its shared UPDATE hook already schedules resolution-cache invalidation.
Choose the change deliberately
| Change | Effect |
|---|---|
| Enable a core feature | Add it to addedFeatures and remove the contrary removal |
| Disable a core feature | Add it to removedFeatures and remove the contrary addition |
Set limitOverrides | Replace the scope-local computed allowance for that identifier |
| Reset features | Clear the manual exception block so base grants apply again |
The named verbs persist the override and invalidate the organization’s cached resolution. Ordinary authorized UPDATE has its own shared postfix hook, registered by the engine; it also schedules invalidation. You do not need to call a named enable/disable verb merely to obtain that hook.
Distinguish an administrative update from a storage repair
Application example: add the registered ADVANCED_REPORTS feature to an existing customer’s agreement. This helper runs inside an authorized super-administrator workflow, with the original external context and a server-resolved organisation identifier. It reads the current override block, changes only the intended feature, and writes through the resource pipeline:
import {
CoreResourceType, type ScopeFeaturesWithOverrides,
} from '@wildo-ai/saas-models';
import type {
ExecutionContext, ServicesRegistryHandlerBackendService,
} from '@wildo-ai/saas-backend-lib';
import { ApplicationFeature } from '@wonder-todos/shared-lib';
export async function grantReportingException(
services: ServicesRegistryHandlerBackendService,
context: ExecutionContext<any>,
organizationId: string,
): Promise<ScopeFeaturesWithOverrides | null> {
const resource = CoreResourceType.ORGANIZATION_FEATURES;
const owner = { organizationId };
const current = await services.read<ScopeFeaturesWithOverrides>(resource, context, owner);
if (!current) throw new Error('Provision the customer entitlement record first');
const featureId = ApplicationFeature.ADVANCED_REPORTS;
const overrides = current.manualOverrides;
return services.update<ScopeFeaturesWithOverrides>(resource, context, owner, {
manualOverrides: {
...overrides,
addedFeatures: [...new Set([...overrides.addedFeatures, featureId])],
removedFeatures: overrides.removedFeatures.filter(id => id !== featureId),
limitOverrides: { ...overrides.limitOverrides },
},
});
}
The feature import is from the Wonder Todos reference application; another application imports its own registered feature identifier. The named core-only enable/disable DTOs cannot express this application feature. The helper preserves unrelated exceptions from the record it read and removes a contradictory denial of reporting. It does not overwrite the subscription-owned base features or limits.
Read followed by UPDATE is not a compare-and-swap: simultaneous administrators could overwrite each other’s changes. Use this example for a controlled administrative workflow, not as a claim of concurrent edit protection. A rejected update or a null result is not a successful grant; the caller must handle it before displaying success. Record the agreement through the application’s administrative evidence flow.
| Path | Persistence and cache responsibility |
|---|---|
| Normal authorized UPDATE | Writes the supplied override change; the shared postfix hook resolves the owner and schedules invalidation |
| Named core-feature enable/disable/reset | Uses the dedicated administrative implementation and its invalidation behavior |
| Direct repository repair | Bypasses the resource hook; the trusted repair must invalidate the exact owner after a successful write |
The generic hook launches FeatureResolutionBackendService.invalidateCache(ResourcePrimaryScope.ORGANIZATIONS, organizationId) without awaiting it and logs failures. A successful UPDATE response is not a promise that every cached consumer is already fresh. Inspect both the stored record and a subsequent effective resolution; do not diagnose the saved override from a screen that still holds an earlier result.
For this example, with no other scope grants: the base features array can remain unchanged while manualOverrides.addedFeatures gains ADVANCED_REPORTS; the refreshed effective feature set then includes reporting. A later billing refresh can replace base grants while retaining the override. Removing this exception restores the base decision—it does not force a denial if a plan or another applicable scope still grants reports.
Preserve the authority boundary
Organisation members can read/search the ledger under its contextual routes. All writes require APP_ADMIN_SUPER_ADMIN, including generic update; an organisation owner cannot promote themselves into a more expensive entitlement. The entitlement fields are marked as audit evidence, allowing the normal resource audit machinery to retain their before/after values.
Manual overrides do not expire automatically. If an agreement is temporary, the application operator must arrange its removal. The request’s reason field is not itself proof that a durable reason record exists; use the application’s verified administrative record for agreement history. The effective context also includes applicable broader-scope grants, so a local removal is not a universal deny across every scope.