Feature Access Management

The Vue frontend does not decide on its own what an agent may see, call or open. The backend evaluates the feature model in features.json for the current agent, tenant, process context and environment, and sends the frontend a tree of API entities: for every feature a name, the URL to call, and whether it is accessible. The frontend never hard-codes backend URLs; it follows the href it was given and hides everything marked inaccessible. This is a HATEOAS-style contract between backend and frontend.

The access-model rationale is in the Solution Strategy. How to add or change a feature step by step is in the Engineering Guardrails. This page explains the runtime flow and where backend and frontend are coupled.

Building blocks

Building block Where Role
features.json Web project Feature tree: each feature has a Name, VisibilityHandlers and Children
Visibility handlers Core/FeatureManager/Handler/ Rules per feature: Azure AD policy, tenant, process context (AppContext), environment, go-live date, downtime, deactivated, and others
FeatureManager Foundation/FeatureManagement/ Loads the tree from features.json, evaluates a feature per request against the FeatureContext (agent, tenant, context, environment). A parent that is invisible makes its whole subtree invisible
FeatureNamen.cs, FeatureNavigation.cs Generated (T4 + scripts/generateFeatureFiles.ps1) Constants for every feature name and typed helpers for every controller action. Never edited by hand
RouteRegistration.cs Web project MapFeature(route, FeatureNamen.X, action) binds a URL to a feature; the feature name becomes the route name
AadAuthorizationHandler + RequiredAzureADGroupPolicyFeatureManager Core/AzureAD/, Core/FeatureManager/ Route gate: checks the feature’s Azure AD policy handlers on every request
API entity generators Generators/*ApiEntityGenerator.cs UI gate: evaluate all handlers of a feature and emit ApiEntityDto { name, href, isInaccessible }, nested per domain
Views/EntryPoint/Index.cshtml Web project Embeds the bootstrap entity as window.apiEntity in the HTML page
Global store ClientApp/src/features/global/store/pinia.ts Holds the API entity tree; exposes is…Inaccessible getters
BaseRepository ClientApp/src/features/shared/repository/ Calls apiEntity.href; rejects without a request if the entity is missing or inaccessible
Router guard ClientApp/src/router.ts Blocks navigation to routes whose feature entity is inaccessible
User-action policies ClientApp/src/composables/allowed-user-actions/ Decide per button or action whether it is allowed, mostly from isInaccessible flags plus item state

Flow

Diagram

Runtime: session start

Diagram

The complete tree is delivered once per session start; there is no lazy loading of entities per domain. A change in feature visibility (for example a go-live date passing) becomes visible in the frontend only after the page is reloaded.

Changing feature visibility

features.json is the only source of the feature tree. Azure App Configuration is loaded as the last configuration source in Azure and refreshed at runtime, but it holds no feature keys and does not override features.json. Changing a feature’s visibility therefore requires a change to features.json and a deployment. Time-based rules such as go-live dates are the exception: they are evaluated per request and take effect without a deployment.

Runtime: calling a feature

Diagram

Two gates, different rules

Both gates use the same feature name, but they do not evaluate the same handlers:

Handler type Route gate (backend URL) UI gate (API entity, router, buttons)
Azure AD policy (RequiredAzureADGroupPolicyHandler, AnyRequiredAzureADGroupPolicyHandler) blocks hides
Tenant, process context, environment, go-live, downtime, deactivated, … does not block hides

Consequences:

  • Tenant-, context- and environment-specific features are hidden in the UI, but their URLs stay callable for every agent with the required role. Where such a restriction is security- or data-relevant, the controller or handler must check it itself.
  • A feature name that is not in features.json is treated as visible (“fails open”); the route then has no access check. A route registered without a feature name is denied and logged as a misconfiguration.
  • A deactivated feature (DeactivatedVisibilityHandler) is only hidden, not switched off on the server.

Frontend: how the decisions are applied

  • API calls. Repositories pass the API entity to BaseRepository; URLs are never built by hand. Inaccessible entities reject before any request is sent.
  • Navigation. The router guard blocks a fixed list of top-level routes (orders, contracts, account, NBA, alerts, legal department, relocations and a few more) when their entity is inaccessible. Sub-pages without an entry in that list rely on the components hiding links and buttons.
  • Buttons and actions. Components ask the user-action policies; see Frontend access management: user actions and policies.
  • Deep links. Entry-point URLs carry the Vue feature and component path. The backend validates them against hard-coded whitelists (VueFeaturePathRouteConstraint, VueComponentPathRouteConstraint) and passes them as InitialRoute; the SPA navigates there after start.

Frontend access management: user actions and policies

API entities say whether a feature is accessible. Whether an agent may click a specific button often depends on more: the role, the state of the concrete item (for example the status of a task), or other client state. Previously every component decided this on its own, so the same rule was implemented several times, drifted apart, and could not be reviewed in one place.

The allowed-user-actions concept (ClientApp/src/composables/allowed-user-actions/) centralises these decisions:

Building block Role
User action A named, type-safe action per business domain, e.g. UserActions.pin.DeletePin. String enums in actions/<domain>/, registered in actions/UserActions.ts
Policy One composable per domain (policies/<domain>/use<X>Policy.ts). Reads an already-loaded source of truth and answers true (allowed), false (denied) or undefined (not my action). Unhandled members of its own enum are denied
Aggregator useIsUserActionAllowed() asks the policies in turn (chain of responsibility). The first answer that is not undefined wins; if no policy owns the action, it is denied
Component Asks only isUserActionAllowed(action, item?) and shows or hides the button. No access logic in components

Diagram

Sources of truth. Policies never call APIs. Most read the backend decision (isInaccessible of an API entity in the global store); some add client-side role checks or item data, checked with type guards. A missing entity counts as inaccessible, so the rules fail closed.

Relation to the backend gates. The policies are a UI layer on top of features.json: they can only make the UI stricter than the API entities, never grant access the backend denies. Item- and role-based rules that exist only in a policy are not enforced by the backend; where they matter for security or data integrity, the backend must check them as well.

Status. Introduced for claims management (debt collection, disconnection, disconnection check, research and contract delimitation tasks), legal department, account data and PIN. As of 2026-10-12 16 files use the aggregator, while about 50 Vue components still check …Inaccessible getters or roles directly. New UI permissions go through a policy; the existing direct checks remain until they are migrated.

The binding conventions and an onboarding guide are in the repository instruction .github/instructions/allowed-user-actions.instructions.md.

Coupling between backend and frontend

Backend and frontend are one deployable unit and cannot be changed independently. The contract is not a versioned API but a set of names that must match on both sides:

Coupling point Backend Frontend What breaks if they drift
API entity keys Name in the generator (e.g. "contractspreload") and property on *ApiEntityDto.cs Property on the TypeScript mirror in features/*/types/*ApiEntityDto.ts Entity is undefined; repository rejects, feature silently disappears
Entity tree shape Generators nest child entities per domain Global store reads initialAppdata.globalApiEntity.<domain>ChildEntity Domain store has no entity
URLs Route constants + MapFeature None — follows href Nothing, as long as the URL is generated; a renamed URL is invisible to the frontend
Vue route names Deep-link whitelists in route constraints Route definitions in router.ts Deep links return 404 or land on the wrong page
Route guard list — getInaccessibleRouteNames() maps store getters to route names New top-level routes are not guarded unless added
Response contracts DTOs, ResponseDetailsDto TypeScript types, is… flags Runtime errors in components
Generated files FeatureNamen.cs, FeatureNavigation.cs — Build errors, or stale names if generation is skipped

The API entity generator snapshot tests (UnitTests/Generators/*.verified.txt) pin the backend side of the entity tree (name, href, isInaccessible). There is no automated check that the TypeScript mirrors match.

Implications

  • Any new endpoint touches both sides: features.json, route, controller action, generator, DTO, TypeScript mirror, store and repository. The scaffold-api-endpoint skill automates this chain.
  • Removing or renaming a feature, entity key or Vue route always needs a coordinated backend and frontend change.
  • The coupling is the main obstacle to the microfrontend direction: a microfrontend would need its own entity contract or a shared, versioned one.