16 KiB
PostFundManagement (PFM) Platform — Architectural Specification
Document Version: 2.0
Target Runtimes: .NET 10.0 (C# 13) | React 19 / TypeScript 5.8 / Vite
Scope: System Architecture, Security Topology, RBAC Enforcement, and Maker-Checker Pattern
1. System Topology & Architectural Layers
The PFM platform is constructed using Clean Architecture and Domain-Driven Design (DDD) principles to guarantee strict decoupling of business rules, persistence concerns, and user interfaces.
┌────────────────────────────────────────────────────────────────────────┐
│ PRESENTATION TIER │
│ PostFundManagement.ConsoleUI │ PostFundManagement.PartnerUI │
│ (Internal Staff & Governance) │ (External Recipient Portal) │
│ React 19 + TypeScript + Vite │ React 19 + WebGL (OGL) + Vite │
└───────────────────────────────────┬────────────────────────────────────┘
│ HTTPS / REST (v1/api/*) + OIDC PKCE
┌───────────────────────────────────▼────────────────────────────────────┐
│ API GATEWAY │
│ PostFundManagement.Api (ASP.NET Core Minimal APIs / Carter) │
│ Authentication: JWT Bearer + OpenTelemetry Observability │
└───────────────────────────────────┬────────────────────────────────────┘
│
┌───────────────────────────────────▼────────────────────────────────────┐
│ APPLICATION LAYER │
│ PostFundManagement.Application │
│ CQRS (Mediator), Business Validators, Domain Services, Quartz Jobs │
└──────────────────┬─────────────────────────────────┬───────────────────┘
│ │
┌──────────────────▼──────────────────┐ ┌──────────▼───────────────────┐
│ DOMAIN LAYER │ │ INFRASTRUCTURE LAYER │
│ PostFundManagement.Domain │ │ PostFundManagement.Infrastr.│
│ Entities, Value Objects, Enums, │ │ PostgreSQL (EF Core 10), │
│ Domain Invariants, Audit Models │ │ Skoruba STS, S3 Garage, │
└─────────────────────────────────────┘ │ Redis HybridCache │
└──────────────────────────────┘
2. Security & Identity Architecture
Authentication and authorization are separated cleanly across system boundaries:
- Security Token Service (STS): Duende IdentityServer / Skoruba Administration (
https://sts.security.khongisa.co.za) issues cryptographically signed JWT access tokens using OpenID Connect (OIDC) with Proof Key for Code Exchange (PKCE). - Audience & Scopes: Web clients request standard OIDC scopes (
openid,profile,roles,email,address) along with the target API resource scope (pfm-api-dev). - Tenant Isolation: Every authenticated principal carries a
tenant_idclaim. Multitenant query filters in EF Core enforce cross-tenant partition boundaries at the data access layer. - Partner Isolation: Grantees and external recipients possess the
partnerrole. TheConsoleUIenforces immediate lockout forpartnerroles, restricting external users exclusively toPartnerUI.
3. Role-Based Access Control (RBAC) Architecture
PFM implements a fine-grained, claim-based RBAC model where system permissions are decomposed into 15 operational domains and 5 progressive action capabilities.
3.1 Role Claims Inventory
| Claim Key | Operational Domain | Functional Scope |
|---|---|---|
dashboard |
Executive Dashboard | Macro portfolio KPIs, disbursed capital, and ISO 31000 risk heatmap |
governance |
Governance & Structure | Holding entities, funder portfolios, programmes, rules, and instruments |
projects_milestones |
Projects & Milestones | Work breakdown structure (WBS), contractual milestones, and activities |
grant_financials |
Grants & Financials | Statutory funding awards, budget lines, contracts, and disbursement tranches |
evidence_risks |
M&E, Evidence & Risks | Deliverable evidence verification queue, indicators, and risk registers |
beneficiaries |
Beneficiaries & Impact | Demographic targets, POPIA compliance items, and outcome deliverables |
change_management |
Change Management | Project issues, contractual change requests, and budget variations |
site_visits |
Site Visits | Physical field inspections, checklist observations, and inspector findings |
settings_invites |
Settings Invites | Onboarding invitations and candidate notifications dispatch |
settings_users |
Settings Users | User provisioning, role assignments, and account lockouts |
settings_auditlog |
Settings Audit Log | Immutable audit ledger and system telemetry records |
settings_notifications |
Settings Notifications | SMTP engines, notification channels, and dispatch telemetry |
tenant_self_service |
Tenant Self-Service | Tenant profile configuration and delegated sub-user management |
tenant_management |
Tenant Management | Multi-tenant directory, partition allocation, and solution ownership |
unrestricted |
System Administration | Root administrative bypass across all system partitions |
3.2 Claim Actions & Capability Hierarchy
Action capabilities within a claim domain follow a strict subset hierarchy:
\text{view} \subset \text{add} \subset \text{update} \quad \text{and} \quad \text{view} \subset \text{export}
view: Read-only access to entity lists, detailed records, metrics, and progress scores.add: Can create new records, edit draft records, submit milestone evidence, and change workflow statuses other than approval (includesview).update: Authoritative governance capability. Can execute approval status transitions (Approved,Activecontracts) on records (includesviewandadd).export: Can generate and download structured spreadsheets (.xlsx) and formal PDF reports (includesview).all: Comprehensive authority (view,add,update,export) over the claim domain.
3.3 Authoritative Role-to-Claims Matrix
| Role | Assigned Claim Domains | Standard Capabilities | Record Approval Authority (update) |
|---|---|---|---|
Admin |
unrestricted |
all |
Yes (System-wide) |
Executive |
dashboard, governance, projects_milestones, grant_financials, evidence_risks, tenant_self_service |
all |
Yes |
Manager |
dashboard, projects_milestones, grant_financials, evidence_risks, beneficiaries, change_management, site_visits, settings_invites, settings_users, settings_auditlog, settings_notifications, tenant_self_service |
all |
Yes |
Supervisor |
dashboard, projects_milestones, grant_financials, evidence_risks, beneficiaries, change_management, site_visits, settings_invites, settings_users |
view, add, update, export(Dashboard: view, export) |
Yes |
Contributor |
projects_milestones, grant_financials, evidence_risks, beneficiaries, change_management, site_visits, settings_invites |
view, add |
❌ Strictly Prohibited |
4. Maker-Checker Pattern Architecture
To eliminate fraud risk, self-dealing, and conflicts of interest, PFM enforces the Maker-Checker Pattern (Segregation of Duties / Dual Authorization).
4.1 Core Principle
The user who creates or drafts a record (the "Maker") is strictly prohibited from verifying or approving that record (the "Checker"), regardless of their administrative role or RBAC claim privileges.
Even if an Executive, Manager, or Supervisor possesses the update claim action on a domain, they cannot approve a record if they are identified as the record's creator.
flowchart LR
A["Maker (User A)<br/>Creates Record"] --> B["Record Persisted<br/>with createdBy = User A"]
B --> C{"Checker Evaluation<br/>(Approver Login)"}
C -->|Checker == User A| D["Approval BLOCKED<br/>Maker-Checker Violation"]
C -->|Checker == User B<br/>with 'update' claim| E["Approval GRANTED<br/>Status -> Approved"]
4.2 Record Attribution Model
Every operational domain entity immutably captures the creator identity:
createdBy: Cryptographic user identifier (Guid / Subject ID) populated from the verified token claimcontext.GetUserId()upon creation.approvedBy: User identifier of the independent verifier populated upon successful transition toApproved.
4.3 Client-Side Enforcement Hooks & Helpers
In PostFundManagement.ConsoleUI, the Maker-Checker rule is evaluated dynamically via useRbac():
isMaker(createdBy): ComparescreatedByagainst current useruser.id,sub, anduser.email.canApproveRecord(claim, createdBy): Evaluates:\text{canApproveRecord} = \text{canApprove}(\text{claim}) \land \neg\text{isMaker}(\text{createdBy})getApprovalRestrictionReason(claim, createdBy): Surfaces explanatory feedback:"Requires update/approval permission"if the claim lacksupdate."Maker-Checker policy: Record creators cannot approve their own records"ifisMakeris true.
4.4 UI Guard Implementations
- Status Dropdowns: The
Approvedoption in status<select>controls is disabled with contextual labels:<option value={ApprovalStatus.Approved} disabled={!canApproveRecord('projects_milestones', record.createdBy)}> Approved {isMaker(record.createdBy) ? '(Maker-Checker: Cannot approve own record)' : ''} </option> - Action Buttons: Fast-track "Approve" buttons (e.g. Budget Approval, Disbursement Authorisation, Evidence Review) are hidden or accompanied by a Maker-Checker indicator badge when viewed by the creator.
- Export Controls:
<ExportButton>enforces domain claim checks explicitly (claim="grant_financials"), rejecting unauthenticated exports.
5. Caching Strategy, Mutation Versioning & Performance Optimization
To achieve sub-50ms page transitions while guaranteeing data freshness across mutations in a distributed architecture, PFM employs a multi-tiered caching topology.
┌────────────────────────────────────────────────────────────────────────┐
│ REACT QUERY CACHE (In-Memory Browser Tier) │
│ staleTime: 2m (dynamic) | 5m (taxonomy) | gcTime: 10m │
└───────────────────────────────────┬────────────────────────────────────┘
│ Cache Miss / Background Refetch
┌───────────────────────────────────▼────────────────────────────────────┐
│ AXIOS CLIENT INTERCEPTOR & MUTATION VERSIONING (_v) │
│ Reads/Browsing: attaches active `_v` epoch token │
│ Mutations (POST/PUT/PATCH/DELETE): auto-bumps epoch -> new `_v` token │
└───────────────────────────────────┬────────────────────────────────────┘
│ HTTP GET /v1/api/*?_v=<epoch>
┌───────────────────────────────────▼────────────────────────────────────┐
│ ASP.NET CORE OUTPUTCACHE (Redis / Memory Tier) │
│ Base Policy: 2-minute expiration partitioned by URI + QueryString │
│ Hits same `_v` -> 2ms response | New `_v` -> DB query & cache refresh │
└────────────────────────────────────────────────────────────────────────┘
5.1 The OutputCache Stale Data Challenge
ASP.NET Core Minimal APIs in PostFundManagement.Api attach .CacheOutput() to GET endpoints, using a 2-minute base sliding expiration backed by Redis / Memory cache.
Because OutputCache keys are generated from the HTTP Method, Host, Path, and Query String, a naive client refetch after a mutation (e.g. queryClient.invalidateQueries({ queryKey: ['projects'] })) calls GET /v1/api/projects. OutputCache intercepts this identical URI, returning the pre-mutation response cached earlier. The updated project status or approved milestone does not reflect in the UI until the 2-minute cache expires.
5.2 Client-Side Mutation Epoch Versioning
PFM solves this without modifying backend endpoints via Client-Side Mutation Versioning:
- Epoch Tracking (
activeMutationEpoch):httpClient.tstracks an active mutation timestamp initialized fromsessionStorageorDate.now(). - Automatic Mutation Interceptor: Every successful mutating HTTP response (
POST,PUT,PATCH,DELETEwith HTTP 2xx) triggersbumpMutationEpoch(). - Query Parameter Injection: Outgoing
GETrequests to internal APIs automatically append_v: getMutationEpoch(). - Instant Invalidation: When a mutation succeeds, the epoch is bumped. Subsequent React Query refetches send the new
_v, producing an immediate cache miss in ASP.NET Core OutputCache. The backend queries PostgreSQL, returns fresh data, and caches the new state under the new_vkey. - Cache Hit Snappiness: Navigations between pages while no mutations have occurred reuse the stable
_v, yielding sub-5ms responses from Redis/Memory OutputCache and 0ms from React Query in-memory cache. - Explicit Freshness (
_t): WhenCache-Control: no-cacheis specified or during explicit refresh operations, a unique timestamp_tis attached to force a guaranteed fresh fetch.
5.3 RBAC Startup Pipeline Acceleration
Following the introduction of fine-grained RBAC and Maker-Checker checks, startup performance was streamlined to eliminate sequential network waterfalls:
- Cache-First Role Claims:
fetchAndCacheSystemRolesandfetchAndCacheRoleClaimscheck in-memory andlocalStoragecaches before dispatching network calls. - Parallel User Resolution:
getUserRolesFromApiandgetUserClaimsFromApiare resolved concurrently usingPromise.allSettled. - Parallel Claim Resolution: Claims across all assigned roles are retrieved concurrently rather than in serial loops.
- Immediate Non-Blocking Session Render: If valid session tokens and user profiles exist in
localStorage,AuthProviderimmediately unblocks UI rendering (isLoading: false) and runs tenant lockout checks, account synchronization, and claim revalidation asynchronously in the background. - In-Memory User Sync & Lockout Caching:
isUserSyncedInBrowserandverifyTenantLockoutmaintain in-memory references to eliminate synchronousJSON.parseoperations from the request pipeline.