Skip to main content

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

FieldValue
Issue typeStory
SummaryCreate GLOBAL and TENANT root ConfigNodes via admin endpoint
Epic linkCONFIG-EPIC-01
StatusTo Do
PriorityMust
Story points5
Labelsservice:config, type:backend, slice:S0
Componentsconfig-node-crud, migrations
FR referencesFR-CONFIG-NODE-001, FR-CONFIG-NODE-005
Legacy FR refsFR-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 no parentId, then the node is created with scopeChain: [] and a 201 response.
  • Given a GLOBAL node exists, when I POST a TENANT node with parentId: globalNodeId, then the node is created with scopeChain: [globalNodeId] and emits config.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 type
  • scopeChain computed 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

FieldValue
Issue typeStory
SummaryReject ConfigNode parent assignment that would create a DAG cycle
Epic linkCONFIG-EPIC-01
StatusTo Do
PriorityMust
Story points3
Labelsservice:config, type:backend, slice:S0
Componentsdag-validation
FR referencesFR-CONFIG-NODE-002
Legacy FR refsFR-CFG-NODE-002
DependenciesCONFIG-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 scopeChain is 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 404 CONFIG_NODE_NOT_FOUND.

Technical notes:

  • Cycle detection uses DFS/BFS traversal of existing parent_id chain before commit
  • scopeChain updated 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

FieldValue
Issue typeStory
SummaryGET /internal/config/resolve executes full 9-step pipeline and returns EvaluationResult
Epic linkCONFIG-EPIC-02
StatusTo Do
PriorityMust
Story points13
Labelsservice:config, type:backend, type:api, slice:S0
Componentsresolution-engine, cache, circuit-breakers
FR referencesFR-CONFIG-RESOLVE-001..009
Legacy FR refsFR-CFG-RESOLVE-001..009
DependenciesCONFIG-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: true is observable in telemetry.
  • Given tenantId in JWT does not match the nodeId tenant, 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

FieldValue
Issue typeStory
SummaryResolution Step 6 delegates to access-policy POST /internal/access/evaluate
Epic linkCONFIG-EPIC-02
StatusTo Do
PriorityMust
Story points5
Labelsservice:config, type:backend, slice:S0
Componentsabac-client, circuit-breakers
FR referencesFR-CONFIG-RESOLVE-007
Legacy FR refsFR-CFG-RESOLVE-007
DependenciesCONFIG-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:

  • HttpABACAdapter calls POST /internal/access/evaluate with { providerId, tenantId, action, resourceNodeId }
  • Config-service does NOT expose POST /internal/access/evaluate itself

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

FieldValue
Issue typeStory
SummaryCache resolution results with TTL; evict on NATS mutation events
Epic linkCONFIG-EPIC-02
StatusTo Do
PriorityMust
Story points8
Labelsservice:config, type:backend, slice:S0
Componentscache, nats-consumer, observability
FR referencesFR-CONFIG-RESOLVE-002, FR-CONFIG-ROLE-005
Legacy FR refsFR-CFG-RESOLVE-002, FR-CFG-ROLE-005
DependenciesCONFIG-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

FieldValue
Issue typeStory
SummaryCreate roles with inheritance edges; BFS expansion bounded to depth 10
Epic linkCONFIG-EPIC-03
StatusTo Do
PriorityMust
Story points8
Labelsservice:config, type:backend, slice:S0
Componentsrole-management, role-bfs
FR referencesFR-CONFIG-ROLE-001..005
Legacy FR refsFR-CFG-ROLE-001..005
DependenciesCONFIG-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) and NURSE, 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 422 ABSTRACT_ROLE_NOT_ASSIGNABLE.

Technical notes:

  • BFS tracks visited role IDs; skips already-visited nodes
  • Role BFS cache: cfg:roles:{tenantId}:{roleKey}:expanded 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-007 — Role Feature Grants with Explicit Allow/Deny

FieldValue
Issue typeStory
SummarySet grantedActions and deniedActions per role per feature; effective_permissions = union(grant) − union(deny)
Epic linkCONFIG-EPIC-03
StatusTo Do
PriorityMust
Story points5
Labelsservice:config, type:backend, slice:S0
Componentsrole-feature-grants
FR referencesFR-CONFIG-ROLE-003
Legacy FR refsFR-CFG-ROLE-003
DependenciesCONFIG-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"] and deniedActions: ["note:sign"], when resolution evaluates action note:sign for a NURSE, then the result is { effect: "deny", reason: "FORBIDDEN" }.
  • Given NURSE inherits note:read from ClinicalStaff via inheritance, when the deniedActions list on NURSE does not include note:read, then note:read is in effective_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

FieldValue
Issue typeStory
SummaryTenant Admin creates ExplicitAllow/Deny override for user at node with justification
Epic linkCONFIG-EPIC-04
StatusTo Do
PriorityMust
Story points5
Labelsservice:config, type:backend, slice:S1
Componentsuser-overrides
FR referencesFR-CONFIG-USR-001, FR-CONFIG-USR-002
Legacy FR refsFR-CFG-USR-001..002
DependenciesCONFIG-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", valid effectiveFrom, and non-empty justification, then the override is created and a config.user_override.created.v1 event is emitted.
  • Given an existing active override for the same (userId, nodeId, featureKey, action), when I attempt to create another, then the API returns 409 OVERRIDE_CONFLICT.
  • Given justification is empty string, when I POST the override, then the API returns 422 with field validation error.
  • Given effectiveTo is 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 userId must 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

FieldValue
Issue typeStory
SummaryExplicitDeny UserNodeOverride cannot be overridden by any role grant or ABAC policy
Epic linkCONFIG-EPIC-04
StatusTo Do
PriorityMust
Story points3
Labelsservice:config, type:backend, slice:S1
Componentsresolution-engine, user-overrides
FR referencesFR-CONFIG-USR-001
Legacy FR refsFR-CFG-USR-001, BR-CFG-002
DependenciesCONFIG-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 for medication: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 ResolveConfigUseCase unit 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

FieldValue
Issue typeStory
SummaryCRUD for UIDefinition (screen/component/element/action-binding)
Epic linkCONFIG-EPIC-05
StatusTo Do
PriorityMust
Story points5
Labelsservice:config, type:backend, slice:S1
Componentsui-definitions
FR referencesFR-CONFIG-UI-004
Legacy FR refsFR-CFG-UI-004
DependenciesCONFIG-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, and actionBinding: "medication:write", then the element is stored and a config.ui_definition.created.v1 event 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_ELEMENT with parentElementId pointing to a UI_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

FieldValue
Issue typeStory
SummaryUIVisibilityRule per role/user; user rule overrides role rule for same element
Epic linkCONFIG-EPIC-05
StatusTo Do
PriorityMust
Story points5
Labelsservice:config, type:backend, slice:S1
Componentsui-visibility-rules, ui-resolver
FR referencesFR-CONFIG-UI-001..003
Legacy FR refsFR-CFG-UI-001..003
DependenciesCONFIG-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 UIVisibilityRule with subjectType: "role" sets add-medication-btn to visible=false for NURSE, when a NURSE calls resolve with includeUI=true, then the UIElementConfig for add-medication-btn has visible: false.
  • Given a user-level UIVisibilityRule sets the same element to visible=true, when that specific user calls resolve, then the user rule wins and visible: true is returned.
  • Given an ExplicitAllow override for medication:write exists for the user at the node, when resolve runs, then add-medication-btn is visible=true, interactable=true for 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

FieldValue
Issue typeStory
SummaryTenant Admin sets scoped design tokens; merged LWW across Global→Tenant→Module→User
Epic linkCONFIG-EPIC-06
StatusTo Do
PriorityShould
Story points5
Labelsservice:config, type:backend, slice:S1
Componentsdesign-tokens, token-resolver
FR referencesFR-CONFIG-DS-001..003
Legacy FR refsFR-CFG-DS-001..003
DependenciesCONFIG-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 call GET /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

FieldValue
Issue typeStory
SummaryLocale-specific tokens (e.g. ps-AF) override locale-agnostic tokens for same key
Epic linkCONFIG-EPIC-06
StatusTo Do
PriorityShould
Story points3
Labelsservice:config, type:backend, slice:S1
Componentsdesign-tokens, locale-support
FR referencesFR-CONFIG-DS-004
Legacy FR refsFR-CFG-DS-004
DependenciesCONFIG-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" with locale: "ps-AF" and a base token font.family.rtl: "Arial" with locale: null, when tokens are resolved with locale=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.