Config Service — API Contracts
Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template · 03 platform-services · 02 DDD
1. Auth Model
| Header | Requirement |
|---|---|
Authorization: Bearer {JWT} | All endpoints; JWT issued by Keycloak |
X-Correlation-Id | Optional; echoed in responses for tracing |
- Internal endpoints (
/internal/*): IP-restricted (service mesh / Kong internal route). JWT still required fortenantIdextraction. - Admin endpoints (
/api/config/*): requireTENANT_ADMINorSUPER_ADMINrole claim. tenantIdis 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
userId | UUID | Yes | User ID (validated against JWT context) |
tenantId | UUID | Yes | Must match JWT claim |
nodeId | UUID | Yes | Hierarchy node where action occurs |
moduleKey | string | Yes | Module key, e.g. CLIN-MEDS |
featureKey | string | Yes | Feature key, e.g. ViewMedications |
action | string | Yes | Action string, e.g. medication:write |
includeUI | boolean | No | Include UI element visibility config |
includeTokens | boolean | No | Include design token map |
locale | string | No | Locale 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:
reason | Cause |
|---|---|
MODULE_NOT_ACTIVE | Module not licensed at node |
FEATURE_DISABLED | Feature flag off for tenant |
FORBIDDEN | No role grant for action |
ABAC_POLICY:{policyId} | ABAC policy denied |
USER_EXPLICIT_DENY | UserNodeOverride deny |
CROSS_TENANT | JWT tenant ≠ node tenant |
DEPENDENCY_UNAVAILABLE | Upstream 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
userId | UUID | Yes | — |
tenantId | UUID | Yes | — |
featureKey | string | Yes | Feature scope to resolve |
nodeId | UUID | No | Node-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:
| Parameter | Type | Required | Description |
|---|---|---|---|
userId | UUID | Yes | — |
tenantId | UUID | Yes | — |
moduleKey | string | Yes | Module scope |
nodeId | UUID | No | Hierarchy node for org token merge |
locale | string | No | Locale 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
| Code | HTTP | Description |
|---|---|---|
CONFIG_NODE_NOT_FOUND | 404 | Config node does not exist or is inactive |
CIRCULAR_ROLE_INHERITANCE | 409 | Inheritance edge creates a cycle |
CONFIG_CIRCULAR_REFERENCE | 409 | Config node parent creates a DAG cycle |
FEATURE_NOT_DEFINED | 404 | featureKey has no definition for this tenant |
OVERRIDE_CONFLICT | 409 | Active override already exists for user/node/action |
ABSTRACT_ROLE_NOT_ASSIGNABLE | 422 | Attempt to directly assign an abstract role |
CONFIG_NODE_HAS_CHILDREN | 409 | Cannot soft-delete node with active children |
RESOLUTION_TIMEOUT | 504 | Full pipeline exceeded 500 ms |
CROSS_TENANT | 200/403 | JWT tenant ≠ node tenant |
MODULE_NOT_ACTIVE | 200 | Module not licensed — in EvaluationResult.reason |
FEATURE_DISABLED | 200 | Feature flag off — in EvaluationResult.reason |
DEPENDENCY_UNAVAILABLE | 503 | Upstream service unreachable — fail closed |
6. Pagination
List endpoints use cursor-based pagination:
| Parameter | Type | Default |
|---|---|---|
cursor | string | — (first page) |
limit | number | 20 |
Response includes meta: { nextCursor?: string, total: number }.