Config Service — User Stories
Service: config-service Story prefix: CONFIG-US Last updated: 2026-04-18
Stories
CONFIG-US-001 — Bootstrap GLOBAL and TENANT ConfigNodes
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Create GLOBAL and TENANT root ConfigNodes via admin endpoint |
| Epic link | CONFIG-EPIC-01 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:config, type:backend, slice:S0 |
| Components | config-node-crud, migrations |
| FR references | FR-CONFIG-NODE-001, FR-CONFIG-NODE-005 |
| Legacy FR refs | FR-CFG-NODE-001, FR-CFG-NODE-005 |
| Dependencies | — |
User story: As a Super Admin, when I bootstrap a new platform tenant, I want to create GLOBAL and TENANT root ConfigNodes so that the entire configuration inheritance chain has valid root anchors.
Acceptance criteria (Gherkin):
- Given I am authenticated as SUPER_ADMIN, when I POST to create a GLOBAL ConfigNode with
nodeType: "GLOBAL"and noparentId, then the node is created withscopeChain: []and a 201 response. - Given a GLOBAL node exists, when I POST a TENANT node with
parentId: globalNodeId, then the node is created withscopeChain: [globalNodeId]and emitsconfig.config.cache_busted.v1. - Given I attempt to hard-delete a ConfigNode, when I send DELETE, then the API returns 405 Method Not Allowed.
- Given I soft-delete a TENANT node, when it has active child nodes, then the API returns 409
CONFIG_NODE_HAS_CHILDREN.
Technical notes:
POST /api/v1/config/nodes— SUPER_ADMIN only for GLOBAL typescopeChaincomputed server-side; never accepted from client- Emit CloudEvents event to NATS after DB commit (outbox pattern)
- Migration:
20260418093000_add_config_nodes_table.sql
Definition of Done:
- Unit + integration tests added; coverage ≥ 80 %.
- OpenAPI contract updated; Pact consumer tests green.
- Event schema registered; schema conformance test green.
- Telemetry spans/metrics added.
- Documentation updated in relevant 17 docs.
CONFIG-US-002 — Cycle Detection for Config Node DAG
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Reject ConfigNode parent assignment that would create a DAG cycle |
| Epic link | CONFIG-EPIC-01 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:config, type:backend, slice:S0 |
| Components | dag-validation |
| FR references | FR-CONFIG-NODE-002 |
| Legacy FR refs | FR-CFG-NODE-002 |
| Dependencies | CONFIG-US-001 |
User story: As a platform engineer, when I configure module and feature nodes, I want the system to reject parent assignments that create cycles so that the configuration graph remains a valid DAG at all times.
Acceptance criteria (Gherkin):
- Given a config node chain A → B → C, when I attempt to set A's parent to C, then the API returns 409
CONFIG_CIRCULAR_REFERENCE. - Given a valid parent assignment, when I POST the node, then
scopeChainis updated to include all ancestors in order. - Given a node with
is_active=false, when I attempt to set it as a parent, then the API returns 404CONFIG_NODE_NOT_FOUND.
Technical notes:
- Cycle detection uses DFS/BFS traversal of existing
parent_idchain before commit scopeChainupdated atomically in same transaction
Definition of Done:
- Unit + integration tests added; coverage ≥ 80 %.
- OpenAPI contract updated; Pact consumer tests green.
- Event schema registered; schema conformance test green.
- Telemetry spans/metrics added.
- Documentation updated in relevant 17 docs.
CONFIG-US-003 — Single-Call Permission Resolution
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | GET /internal/config/resolve executes full 9-step pipeline and returns EvaluationResult |
| Epic link | CONFIG-EPIC-02 |
| Status | To Do |
| Priority | Must |
| Story points | 13 |
| Labels | service:config, type:backend, type:api, slice:S0 |
| Components | resolution-engine, cache, circuit-breakers |
| FR references | FR-CONFIG-RESOLVE-001..009 |
| Legacy FR refs | FR-CFG-RESOLVE-001..009 |
| Dependencies | CONFIG-US-001, CONFIG-US-006 |
User story: As a platform service, when I need to determine whether a user can perform an action at a facility node, I want to call a single endpoint and receive a definitive allow/deny result with data scope so that I do not need to make multiple API calls.
Acceptance criteria (Gherkin):
- Given a licensed module, enabled feature, and role with the required grant, when I call
GET /internal/config/resolve, then the response is{ effect: "allow", reason: "ROLE_GRANT", dataScope: "sameFacility" }. - Given a module with no active license at the node, when I call resolve, then the response is
{ effect: "deny", reason: "MODULE_NOT_ACTIVE" }and no further steps are evaluated. - Given a disabled feature flag, when I call resolve, then the response is
{ effect: "deny", reason: "FEATURE_DISABLED" }. - Given a warm Redis cache for this request key, when I call resolve, then the response is returned < 20 ms and
cached: trueis observable in telemetry. - Given
tenantIdin JWT does not match thenodeIdtenant, when I call resolve, then the response is{ effect: "deny", reason: "CROSS_TENANT" }.
Technical notes:
- Cache key:
cfg:{tenantId}:{userId}:{nodeId}:{moduleKey}:{featureKey}TTL 60 s - Fail closed on any upstream failure:
DEPENDENCY_UNAVAILABLE - Circuit breaker on facility-service, platform-admin, access-policy
- Resolution timeout SLO: p95 < 100 ms
Definition of Done:
- Unit + integration tests added; coverage ≥ 80 %.
- OpenAPI contract updated; Pact consumer tests green.
- Event schema registered; schema conformance test green.
- Telemetry spans/metrics added.
- Documentation updated in relevant 17 docs.
CONFIG-US-004 — ABAC Delegation to Access-Policy
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Resolution Step 6 delegates to access-policy POST /internal/access/evaluate |
| Epic link | CONFIG-EPIC-02 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:config, type:backend, slice:S0 |
| Components | abac-client, circuit-breakers |
| FR references | FR-CONFIG-RESOLVE-007 |
| Legacy FR refs | FR-CFG-RESOLVE-007 |
| Dependencies | CONFIG-US-003, cross-service: access-policy |
User story: As the platform, when evaluating permissions, I want config-service to delegate ABAC evaluation to the access-policy service so that ABAC ownership remains with a single service and config-service does not expose a parallel evaluate endpoint.
Acceptance criteria (Gherkin):
- Given access-policy returns
{ effect: "deny", reason: "...", policyId: "..." }, when resolve is called, then the EvaluationResult carries that deny with the policyId. - Given access-policy is unavailable, when resolve is called, then the result is
{ effect: "deny", reason: "DEPENDENCY_UNAVAILABLE" }and a circuit-breaker event is logged. - Given access-policy returns allow, when no user override exists, then the resolution continues to Step 7 (user override check).
Technical notes:
HttpABACAdaptercallsPOST /internal/access/evaluatewith{ providerId, tenantId, action, resourceNodeId }- Config-service does NOT expose
POST /internal/access/evaluateitself
Definition of Done:
- Unit + integration tests added; coverage ≥ 80 %.
- OpenAPI contract updated; Pact consumer tests green.
- Event schema registered; schema conformance test green.
- Telemetry spans/metrics added.
- Documentation updated in relevant 17 docs.
CONFIG-US-005 — Redis Cache with NATS-Driven Eviction
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Cache resolution results with TTL; evict on NATS mutation events |
| Epic link | CONFIG-EPIC-02 |
| Status | To Do |
| Priority | Must |
| Story points | 8 |
| Labels | service:config, type:backend, slice:S0 |
| Components | cache, nats-consumer, observability |
| FR references | FR-CONFIG-RESOLVE-002, FR-CONFIG-ROLE-005 |
| Legacy FR refs | FR-CFG-RESOLVE-002, FR-CFG-ROLE-005 |
| Dependencies | CONFIG-US-003 |
User story: As an SRE, when config mutations occur, I want the Redis cache to be invalidated via NATS events so that callers always receive up-to-date resolution results within one TTL cycle.
Acceptance criteria (Gherkin):
- Given a feature definition is updated, when the NATS event is received, then all matching cache keys for that tenant/module/feature are evicted within 1 s.
- Given Redis is unavailable, when a resolution is requested, then the full pipeline executes without Redis and returns a valid result (latency SLO may be exceeded, alert fires).
- Given cache eviction fails, when DLQ depth exceeds 10, then an alert fires.
Technical notes:
- NATS consumer subscribes to
config.feature.*,config.role.*,config.user_override.*,config.design_token.* - After eviction, publish
config.config.cache_busted.v1
Definition of Done:
- Unit + integration tests added; coverage ≥ 80 %.
- OpenAPI contract updated; Pact consumer tests green.
- Event schema registered; schema conformance test green.
- Telemetry spans/metrics added.
- Documentation updated in relevant 17 docs.
CONFIG-US-006 — Role Definition and Inheritance Graph
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Create roles with inheritance edges; BFS expansion bounded to depth 10 |
| Epic link | CONFIG-EPIC-03 |
| Status | To Do |
| Priority | Must |
| Story points | 8 |
| Labels | service:config, type:backend, slice:S0 |
| Components | role-management, role-bfs |
| FR references | FR-CONFIG-ROLE-001..005 |
| Legacy FR refs | FR-CFG-ROLE-001..005 |
| Dependencies | CONFIG-US-001 |
User story: As a Tenant Admin, when I model clinical staff roles, I want to define role inheritance so that NURSE inherits base grants from ClinicalStaff without duplicating the grant list.
Acceptance criteria (Gherkin):
- Given I create
ClinicalStaff(isAbstract=true) andNURSE, when I add inheritance edge NURSE → ClinicalStaff, then BFS expansion includes ClinicalStaff grants in NURSE's effective permissions. - Given a role inheritance chain of depth 11, when I attempt to add the depth-11 edge, then the API returns 422 with
ROLE_DEPTH_EXCEEDED. - Given an existing edge A → B, when I attempt to add B → A, then the API returns 409
CIRCULAR_ROLE_INHERITANCE. - Given a role with
isAbstract=true, when I attempt to assign it directly to a user, then the API returns 422ABSTRACT_ROLE_NOT_ASSIGNABLE.
Technical notes:
- BFS tracks visited role IDs; skips already-visited nodes
- Role BFS cache:
cfg:roles:{tenantId}:{roleKey}:expandedTTL 300 s
Definition of Done:
- Unit + integration tests added; coverage ≥ 80 %.
- OpenAPI contract updated; Pact consumer tests green.
- Event schema registered; schema conformance test green.
- Telemetry spans/metrics added.
- Documentation updated in relevant 17 docs.
CONFIG-US-007 — Role Feature Grants with Explicit Allow/Deny
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Set grantedActions and deniedActions per role per feature; effective_permissions = union(grant) − union(deny) |
| Epic link | CONFIG-EPIC-03 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:config, type:backend, slice:S0 |
| Components | role-feature-grants |
| FR references | FR-CONFIG-ROLE-003 |
| Legacy FR refs | FR-CFG-ROLE-003 |
| Dependencies | CONFIG-US-006 |
User story:
As a Tenant Admin, when I configure nurse permissions, I want to explicitly grant note:write but deny note:sign for the NURSE role so that nurses can write but not countersign clinical notes.
Acceptance criteria (Gherkin):
- Given NURSE role has
grantedActions: ["note:write"]anddeniedActions: ["note:sign"], when resolution evaluates actionnote:signfor a NURSE, then the result is{ effect: "deny", reason: "FORBIDDEN" }. - Given NURSE inherits
note:readfrom ClinicalStaff via inheritance, when thedeniedActionslist on NURSE does not includenote:read, thennote:readis ineffective_permissions. - Given I POST a feature grant with the same
(roleId, featureKey)pair as an existing grant, when the operation succeeds, then the existing grant is replaced (idempotent upsert).
Technical notes:
effective_permissions = union(grantedActions across all expanded roles) − union(deniedActions across all expanded roles)- Grant upsert:
ON CONFLICT (tenant_id, role_id, feature_key) DO UPDATE
Definition of Done:
- Unit + integration tests added; coverage ≥ 80 %.
- OpenAPI contract updated; Pact consumer tests green.
- Event schema registered; schema conformance test green.
- Telemetry spans/metrics added.
- Documentation updated in relevant 17 docs.
CONFIG-US-008 — Create Time-Bounded User Node Override
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Tenant Admin creates ExplicitAllow/Deny override for user at node with justification |
| Epic link | CONFIG-EPIC-04 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:config, type:backend, slice:S1 |
| Components | user-overrides |
| FR references | FR-CONFIG-USR-001, FR-CONFIG-USR-002 |
| Legacy FR refs | FR-CFG-USR-001..002 |
| Dependencies | CONFIG-US-003 |
User story: As a Tenant Admin, when a nurse needs emergency medication dispensing access at a specific ward, I want to create a time-bounded ExplicitAllow with justification so that the nurse can perform the action at that node only for the authorized period.
Acceptance criteria (Gherkin):
- Given I POST an override with
effect: "allow", valideffectiveFrom, and non-emptyjustification, then the override is created and aconfig.user_override.created.v1event is emitted. - Given an existing active override for the same
(userId, nodeId, featureKey, action), when I attempt to create another, then the API returns 409OVERRIDE_CONFLICT. - Given
justificationis empty string, when I POST the override, then the API returns 422 with field validation error. - Given
effectiveTois in the past, when resolution evaluates the override, then it is silently ignored (no effect on result).
Technical notes:
POST /api/v1/config/users/:userId/overrides- Target
userIdmust belong to same tenant as caller (validated via identity-service)
Definition of Done:
- Unit + integration tests added; coverage ≥ 80 %.
- OpenAPI contract updated; Pact consumer tests green.
- Event schema registered; schema conformance test green.
- Telemetry spans/metrics added.
- Documentation updated in relevant 17 docs.
CONFIG-US-009 — ExplicitDeny Override is Final
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | ExplicitDeny UserNodeOverride cannot be overridden by any role grant or ABAC policy |
| Epic link | CONFIG-EPIC-04 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:config, type:backend, slice:S1 |
| Components | resolution-engine, user-overrides |
| FR references | FR-CONFIG-USR-001 |
| Legacy FR refs | FR-CFG-USR-001, BR-CFG-002 |
| Dependencies | CONFIG-US-008 |
User story: As a Compliance Officer, when a specific user must be denied an action regardless of their role, I want an ExplicitDeny override to be absolutely final so that no role grant or ABAC policy can circumvent it.
Acceptance criteria (Gherkin):
- Given a user has a role with
grantedActions: ["medication:write"]AND an active ExplicitDeny override formedication:write, when resolution is called, then the result is{ effect: "deny", reason: "USER_EXPLICIT_DENY" }. - Given a user has an ExplicitAllow override AND an active ExplicitDeny for the same action at the same node, when resolution evaluates Step 7, then deny takes precedence.
Technical notes:
- BR-CFG-002: ExplicitDeny at Step 7 is final — no override mechanism
- Covered in
ResolveConfigUseCaseunit test suite
Definition of Done:
- Unit + integration tests added; coverage ≥ 80 %.
- OpenAPI contract updated; Pact consumer tests green.
- Event schema registered; schema conformance test green.
- Telemetry spans/metrics added.
- Documentation updated in relevant 17 docs.
CONFIG-US-010 — Register UI Element Definitions
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | CRUD for UIDefinition (screen/component/element/action-binding) |
| Epic link | CONFIG-EPIC-05 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:config, type:backend, slice:S1 |
| Components | ui-definitions |
| FR references | FR-CONFIG-UI-004 |
| Legacy FR refs | FR-CFG-UI-004 |
| Dependencies | CONFIG-US-001 |
User story: As a Tenant Admin, when I configure the platform UI, I want to register UI elements and their action bindings so that the resolution engine can return role-appropriate UI visibility trees.
Acceptance criteria (Gherkin):
- Given I POST a UI element with
elementType: "element",featureKey, andactionBinding: "medication:write", then the element is stored and aconfig.ui_definition.created.v1event is emitted. - Given I POST a parent-child hierarchy (screen → component → element), when I call
GET /internal/config/ui, then the tree is returned correctly nested. - Given I attempt to create a
UI_ELEMENTwithparentElementIdpointing to aUI_SCREEN, then the API returns 422 (invalid parent type).
Technical notes:
- Valid parent types enforced per taxonomy
- Cache:
cfg:ui:{tenantId}:{userId}:{featureKey}TTL 120 s
Definition of Done:
- Unit + integration tests added; coverage ≥ 80 %.
- OpenAPI contract updated; Pact consumer tests green.
- Event schema registered; schema conformance test green.
- Telemetry spans/metrics added.
- Documentation updated in relevant 17 docs.
CONFIG-US-011 — Role and User UI Visibility Rules
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | UIVisibilityRule per role/user; user rule overrides role rule for same element |
| Epic link | CONFIG-EPIC-05 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:config, type:backend, slice:S1 |
| Components | ui-visibility-rules, ui-resolver |
| FR references | FR-CONFIG-UI-001..003 |
| Legacy FR refs | FR-CFG-UI-001..003 |
| Dependencies | CONFIG-US-010 |
User story:
As a Tenant Admin, when I configure the NURSE role, I want to set add-medication-btn to visible=false for that role so that nurses do not see the add medication button unless they have an explicit user override.
Acceptance criteria (Gherkin):
- Given a
UIVisibilityRulewithsubjectType: "role"setsadd-medication-btntovisible=falsefor NURSE, when a NURSE calls resolve withincludeUI=true, then the UIElementConfig foradd-medication-btnhasvisible: false. - Given a user-level
UIVisibilityRulesets the same element tovisible=true, when that specific user calls resolve, then the user rule wins andvisible: trueis returned. - Given an ExplicitAllow override for
medication:writeexists for the user at the node, when resolve runs, thenadd-medication-btnisvisible=true, interactable=truefor that user.
Technical notes:
- User rule priority over role rule per FR-CONFIG-UI-003
- ExplicitAllow cascade to bound elements per FR-CONFIG-USR-003
Definition of Done:
- Unit + integration tests added; coverage ≥ 80 %.
- OpenAPI contract updated; Pact consumer tests green.
- Event schema registered; schema conformance test green.
- Telemetry spans/metrics added.
- Documentation updated in relevant 17 docs.
CONFIG-US-012 — Tenant Design Token Management
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Tenant Admin sets scoped design tokens; merged LWW across Global→Tenant→Module→User |
| Epic link | CONFIG-EPIC-06 |
| Status | To Do |
| Priority | Should |
| Story points | 5 |
| Labels | service:config, type:backend, slice:S1 |
| Components | design-tokens, token-resolver |
| FR references | FR-CONFIG-DS-001..003 |
| Legacy FR refs | FR-CFG-DS-001..003 |
| Dependencies | CONFIG-US-001 |
User story:
As a Tenant Admin for Afghanistan MoPH, when I configure the platform, I want to set brand.primary: #006600 at tenant scope so that all services and frontend components receive MoPH institutional green via the token merge chain.
Acceptance criteria (Gherkin):
- Given a tenant-scoped token
brand.primary: #006600, when I callGET /internal/config/tokens, then the response includes"brand.primary": "#006600". - Given a module-scoped token overrides the same key, when the module scope is requested, then the module value (LWW) is returned.
- Given a global token exists and a tenant token overrides it, when tokens are resolved at tenant scope, then tenant value wins over global value.
Technical notes:
- Merge priority: User > Module > OrgNode > Tenant > Global
- Cache:
cfg:tokens:{tenantId}:{userId}:{moduleKey}:{locale}:{nodeId}TTL 300 s
Definition of Done:
- Unit + integration tests added; coverage ≥ 80 %.
- OpenAPI contract updated; Pact consumer tests green.
- Event schema registered; schema conformance test green.
- Telemetry spans/metrics added.
- Documentation updated in relevant 17 docs.
CONFIG-US-013 — Locale-Scoped Design Token Overrides
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Locale-specific tokens (e.g. ps-AF) override locale-agnostic tokens for same key |
| Epic link | CONFIG-EPIC-06 |
| Status | To Do |
| Priority | Should |
| Story points | 3 |
| Labels | service:config, type:backend, slice:S1 |
| Components | design-tokens, locale-support |
| FR references | FR-CONFIG-DS-004 |
| Legacy FR refs | FR-CFG-DS-004 |
| Dependencies | CONFIG-US-012 |
User story:
As a platform engineer supporting Afghanistan (ps-AF locale), when the user's locale is Pashto, I want font.family.rtl to resolve to Noto Nastaliq Urdu so that clinical documents and UI render with the correct script.
Acceptance criteria (Gherkin):
- Given a tenant token
font.family.rtl: "Noto Nastaliq Urdu"withlocale: "ps-AF"and a base tokenfont.family.rtl: "Arial"withlocale: null, when tokens are resolved withlocale=ps-AF, then"font.family.rtl": "Noto Nastaliq Urdu"is returned. - Given the same request with
locale=en-US, when tokens are resolved, then the locale-agnostic value"Arial"is returned. - Given a valid nodeId for the tenant, when tokens are resolved with that nodeId, then org-scoped tokens at that node and its ancestors are included in the merge.
Technical notes:
- Locale filter applied after scope-tier merge
- FR-CONFIG-DS-004
Definition of Done:
- Unit + integration tests added; coverage ≥ 80 %.
- OpenAPI contract updated; Pact consumer tests green.
- Event schema registered; schema conformance test green.
- Telemetry spans/metrics added.
- Documentation updated in relevant 17 docs.