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 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 |


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.
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.

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:
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.DeactivatedVisibilityHandler) is only hidden, not switched off on the
server.BaseRepository; URLs are never built by
hand. Inaccessible entities reject before any request is sent.VueFeaturePathRouteConstraint,
VueComponentPathRouteConstraint) and passes them as InitialRoute; the SPA navigates there after
start.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 |

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.
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.
features.json, route, controller action, generator, DTO,
TypeScript mirror, store and repository. The scaffold-api-endpoint skill automates this chain.