Skip to main content
Wildo.ai Coming soon

Plans, entitlements and usage

Keep customer agreements separate from the plan

Give a customer specific feature or allowance overrides without changing the catalogue.

A customer’s plan grants and separately recorded agreement combine into its access.

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

ChangeEffect
Enable a core featureAdd it to addedFeatures and remove the contrary removal
Disable a core featureAdd it to removedFeatures and remove the contrary addition
Set limitOverridesReplace the scope-local computed allowance for that identifier
Reset featuresClear 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.

PathPersistence and cache responsibility
Normal authorized UPDATEWrites the supplied override change; the shared postfix hook resolves the owner and schedules invalidation
Named core-feature enable/disable/resetUses the dedicated administrative implementation and its invalidation behavior
Direct repository repairBypasses 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.

Building a B2B product or an internal tool?

Wildo is not self-service yet. Tell us what you have in mind and we will say plainly whether it fits, and what happens next.