Skip to main content

Config Service — API Contracts

Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template · 03 platform-services · 02 DDD


1. Auth Model

HeaderRequirement
Authorization: Bearer {JWT}All endpoints; JWT issued by Keycloak
X-Correlation-IdOptional; echoed in responses for tracing
  • Internal endpoints (/internal/*): IP-restricted (service mesh / Kong internal route). JWT still required for tenantId extraction.
  • Admin endpoints (/api/config/*): require TENANT_ADMIN or SUPER_ADMIN role claim.
  • tenantId is always extracted from the JWT claim. It is never accepted from query parameters in the user-facing path.

2. Internal Endpoints (Service-to-Service)

2.1 GET /internal/config/resolve

Purpose: Execute the full 9-step resolution pipeline. Primary endpoint for all platform services and frontend SDK.

Auth: Service-to-service JWT (internal claim)

Query Parameters:

ParameterTypeRequiredDescription
userIdUUIDYesUser ID (validated against JWT context)
tenantIdUUIDYesMust match JWT claim
nodeIdUUIDYesHierarchy node where action occurs
moduleKeystringYesModule key, e.g. CLIN-MEDS
featureKeystringYesFeature key, e.g. ViewMedications
actionstringYesAction string, e.g. medication:write
includeUIbooleanNoInclude UI element visibility config
includeTokensbooleanNoInclude design token map
localestringNoLocale for token resolution, e.g. ps-AF

Response 200 — Allow (minimal):

{
"effect": "allow",
"reason": "ROLE_GRANT",
"policyId": null,
"dataScope": "sameFacility"
}

Response 200 — Allow with UI + tokens:

{
"effect": "allow",
"reason": "USER_EXPLICIT_ALLOW",
"policyId": null,
"dataScope": "sameFacility",
"uiConfig": [
{
"elementKey": "add-medication-btn",
"elementType": "element",
"visible": true,
"interactable": true,
"actionBinding": "medication:write",
"children": []
}
],
"designTokens": {
"brand.primary": "#006600",
"font.family.rtl": "Noto Nastaliq Urdu",
"layout.direction": "rtl"
}
}

Response 200 — Deny variants:

reasonCause
MODULE_NOT_ACTIVEModule not licensed at node
FEATURE_DISABLEDFeature flag off for tenant
FORBIDDENNo role grant for action
ABAC_POLICY:{policyId}ABAC policy denied
USER_EXPLICIT_DENYUserNodeOverride deny
CROSS_TENANTJWT tenant ≠ node tenant
DEPENDENCY_UNAVAILABLEUpstream service down (fail closed)

Response 504: RESOLUTION_TIMEOUT — pipeline exceeded 500 ms.


2.2 GET /internal/config/ui

Purpose: Return UI element visibility tree for a feature scope. Does not re-run Steps 1–7.

Query Parameters:

ParameterTypeRequiredDescription
userIdUUIDYes
tenantIdUUIDYes
featureKeystringYesFeature scope to resolve
nodeIdUUIDNoNode-scoped visibility rules applied when present

Response 200:

[
{
"elementKey": "ClinicalNotesPage",
"elementType": "screen",
"visible": true,
"interactable": true,
"actionBinding": null,
"children": [
{
"elementKey": "new-note-btn",
"elementType": "element",
"visible": true,
"interactable": true,
"actionBinding": "note:write",
"children": []
},
{
"elementKey": "sign-note-btn",
"elementType": "element",
"visible": false,
"interactable": false,
"actionBinding": "note:sign",
"children": []
}
]
}
]

Cache key: cfg:ui:{tenantId}:{userId}:{featureKey} TTL 120 s.


2.3 GET /internal/config/tokens

Purpose: Return merged design token map for a module scope.

Query Parameters:

ParameterTypeRequiredDescription
userIdUUIDYes
tenantIdUUIDYes
moduleKeystringYesModule scope
nodeIdUUIDNoHierarchy node for org token merge
localestringNoLocale for locale-scoped overrides

Response 200:

{
"brand.primary": "#006600",
"brand.secondary": "#004400",
"font.family.latin": "Noto Sans",
"font.family.rtl": "Noto Nastaliq Urdu",
"layout.direction": "rtl",
"clinical.alert.critical": "#B71C1C"
}

Response 403 — nodeId not in tenant:

{
"statusCode": 403,
"code": "CROSS_TENANT",
"message": "nodeId is not valid for the given tenantId"
}

Cache key: cfg:tokens:{tenantId}:{userId}:{moduleKey}:{locale|default}:{nodeId|none} TTL 300 s.


3. Admin Endpoints (Tenant Admin / Super Admin)

Base path: POST /api/v1/config/... via Kong. All require Authorization: Bearer {JWT} with TENANT_ADMIN or SUPER_ADMIN role.


3.1 Feature Definitions

POST /api/v1/config/modules/:moduleKey/features

Auth: TENANT_ADMIN

Request:

{
"featureKey": "ViewMedications",
"allowedActions": ["medication:read"],
"dataScopeType": "sameFacility",
"description": "View a patient's medication list"
}

Response 201:

{
"id": "feat_01JRXXXX",
"tenantId": "ten_afg_moph_001",
"featureKey": "ViewMedications",
"moduleKey": "CLIN-MEDS",
"allowedActions": ["medication:read"],
"dataScopeType": "sameFacility",
"isActive": true,
"createdAt": "2026-04-18T09:00:00Z"
}

Errors: 404 MODULE_NOT_FOUND, 409 feature key already exists for tenant.


PATCH /api/v1/config/features/:featureKey

Auth: TENANT_ADMIN

Request:

{
"allowedActions": ["medication:read", "medication:list"],
"dataScopeType": "facilityOnly"
}

Response 200: Updated feature definition object.


3.2 Role Definitions

POST /api/v1/config/roles

Auth: SUPER_ADMIN for isSystem=true; TENANT_ADMIN otherwise.

Request:

{
"roleKey": "ClinicalStaff",
"displayName": "Clinical Staff",
"isAbstract": true,
"isSystem": false
}

Response 201: Role definition object.


PATCH /api/v1/config/roles/:roleKey

Auth: SUPER_ADMIN for system roles; TENANT_ADMIN for custom roles.

Request: { "displayName": "Clinical Staff (Extended)" }

Response 200: Updated role definition object.


POST /api/v1/config/roles/:roleKey/inheritance

Auth: SUPER_ADMIN

Request:

{
"parentRoleKey": "ClinicalStaff",
"inheritanceType": "full"
}

Response 201: Inheritance edge object.

Response 409:

{
"error": {
"code": "CIRCULAR_ROLE_INHERITANCE",
"message": "Adding this edge would create a cycle",
"details": { "cyclePath": ["NURSE", "ClinicalStaff", "NURSE"] }
}
}

POST /api/v1/config/roles/:roleKey/feature-grants

Auth: TENANT_ADMIN. Idempotent — replaces existing grants for role+feature.

Request:

{
"featureKey": "ManageClinicalNotes",
"grantedActions": ["note:read", "note:write"],
"deniedActions": ["note:sign"]
}

Response 201: Grant object.


3.3 User Node Overrides

POST /api/v1/config/users/:userId/overrides

Auth: TENANT_ADMIN. Target userId must belong to same tenant.

Request:

{
"nodeId": "cfgn_01JRXXXX",
"featureKey": "ViewMedications",
"action": "medication:write",
"effect": "allow",
"justification": "Night dispensing protocol — SDK Ward A emergency authorization",
"effectiveFrom": "2026-01-01",
"effectiveTo": "2026-12-31"
}

Response 201: Override object with id, grantedBy, createdAt.

Response 409: OVERRIDE_CONFLICT — active override already exists for same user/node/action.


GET /api/v1/config/users/:userId/overrides

Auth: TENANT_ADMIN

Response 200: { "data": [OverrideObject[]], "meta": { "total": N } }


DELETE /api/v1/config/users/:userId/overrides/:id

Auth: TENANT_ADMIN

Response 204: No content. Soft-deletes the override.


3.4 Design Tokens

POST /api/v1/config/design-tokens

Auth: TENANT_ADMIN for tenant/module/user scope; SUPER_ADMIN for global.

Request:

{
"scopeType": "tenant",
"tokenKey": "brand.primary",
"tokenValue": "#006600",
"locale": null
}

Response 201: Token object.


GET /api/v1/config/design-tokens

Auth: TENANT_ADMIN

Query: ?scopeType=tenant|module|user

Response 200: { "data": [TokenObject[]] }


4. Standard Error Envelope

All errors use:

{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable message",
"details": {}
},
"correlationId": "req_cfg_xxx",
"timestamp": "2026-04-18T09:00:00Z"
}

5. Service-Specific Error Codes

CodeHTTPDescription
CONFIG_NODE_NOT_FOUND404Config node does not exist or is inactive
CIRCULAR_ROLE_INHERITANCE409Inheritance edge creates a cycle
CONFIG_CIRCULAR_REFERENCE409Config node parent creates a DAG cycle
FEATURE_NOT_DEFINED404featureKey has no definition for this tenant
OVERRIDE_CONFLICT409Active override already exists for user/node/action
ABSTRACT_ROLE_NOT_ASSIGNABLE422Attempt to directly assign an abstract role
CONFIG_NODE_HAS_CHILDREN409Cannot soft-delete node with active children
RESOLUTION_TIMEOUT504Full pipeline exceeded 500 ms
CROSS_TENANT200/403JWT tenant ≠ node tenant
MODULE_NOT_ACTIVE200Module not licensed — in EvaluationResult.reason
FEATURE_DISABLED200Feature flag off — in EvaluationResult.reason
DEPENDENCY_UNAVAILABLE503Upstream service unreachable — fail closed

6. Pagination

List endpoints use cursor-based pagination:

ParameterTypeDefault
cursorstring— (first page)
limitnumber20

Response includes meta: { nextCursor?: string, total: number }.