PRMS is a TypeScript monorepo containing two applications: an Angular 19 single-page application served by Nginx and a NestJS 11 API deployed to AWS Lambda or Docker. This page covers the full-stack architecture, key design decisions, external integrations, and the role model that governs access control. Understanding this structure helps both contributors and integrators work with PRMS reliably.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/AllianceBioversityCIAT/onecgiar_pr/llms.txt
Use this file to discover all available pages before exploring further.
System overview
Frontend: Angular 19 SPA
The client application lives inonecgiar-pr-client/ and is built with Angular 19 and PrimeNG 19. It is compiled to a static bundle and served by Nginx in production.
Page module structure
Each feature lives in its own Angular module under
src/app/pages/<feature>/. Modules lazy-load via feature routing modules. Route guards enforce auth and role-based access at the navigation layer.State management
State is held in services using Angular signals and
BehaviorSubject — no NgRx store. Phase context is shell-level and propagated through query parameters on navigation.HTTP interceptor
A single interceptor at
shared/interceptors/general-interceptor.service.ts attaches the auth: <JWT> header to every outgoing request. Elasticsearch URLs are excluded from this header injection.Real-time updates
The client subscribes to live events via
ngx-socket-io and pusher-js. Events include new QA assignments, submission updates, and result lock or unlock signals.Backend: NestJS 11
The server lives inonecgiar-pr-server/ and follows a module-per-feature pattern. Every domain feature is a NestJS module with a controller, service, and repository. The entire application is wired in app.module.ts and routed in main.routes.ts.
Routing surface
| Mount path | Auth | Notes |
|---|---|---|
/api/* | JWT required | Main application traffic — results, QA, IPSR, notifications, admin |
/v2/api/* | JWT required | Forward-compatible versioned endpoints |
/auth/* | Public (login paths) | Login, token refresh, user and role management |
/clarisa/* | JWT required | CLARISA catalog proxy and scheduled sync |
/toc/* | JWT required | Theory of Change trees and result-to-ToC mappings |
/api/bilateral/* | JWT excluded | Headless typed payload surface for bilateral funders |
/api/platform-report/* | JWT excluded | Headless phase-scoped payload surface for platform reports |
/type-one-report | JWT required | PMU consolidated report (bound separately) |
/logs/* | Internal | DynamoDB operational log surface |
Middleware pipeline
Every request to a JWT-protected path flows throughJwtMiddleware, which:
- Reads the custom
authheader and verifies the token usingJWT_SKEY. - Rejects
Basicauth andAuthorization: Bearerpatterns — only the customauthheader is accepted. - On valid token, re-signs and returns a fresh
authheader on the response (rolling session). The Angular interceptor picks this up automatically. - Exposes the decoded payload as
req.userwith fieldsidandemail.
Deployment modes
PRMS supports two backend deployment targets.- AWS Lambda (Serverless)
- Docker container
The primary production deployment uses the Serverless Framework (
serverless.yaml). The NestJS application is compiled and bundled as a single Lambda function with the handler at dist/lambda.handler.Key considerations:- Cold start budget applies — avoid adding heavy dependencies without measuring impact.
serverless-plugin-optimizeandserverless-plugin-typescriptare active in the build chain.- Lambda logs go to CloudWatch. Operational events also land in DynamoDB via the logs module.
- Global throttler (60 seconds / 100 requests) is active. Bilateral routes are exempt via
ThrottlerExcludeBilateralGuard.
Data layer
MySQL is the system of record. All result data, user roles, phase state, audit history, and CLARISA catalog caches are stored in MySQL 8 via TypeORM. DynamoDB is used exclusively for operational logs and is not a source of truth for any business entity.
onecgiar-pr-server/src/migrations/. The migration:check:ci script blocks merges that introduce entity drift without a corresponding migration. Never edit the production schema by hand.
Every result entity carries:
result_code— stable public identifier.result_type_id— one of the 11 canonical types.status_id— current workflow state (1–7).version_id— the reporting phase this result belongs to.is_active— soft-delete flag. Deletes flip this tofalse; they never hard-delete records without admin action.
External integrations
PRMS integrates with — but does not own — these external systems.CLARISA (master data)
CLARISA (master data)
CLARISA is the source of truth for institutional catalogs: centers, initiatives, partner organizations, countries, regions, policy types, innovation readiness levels, and more. PRMS consumes CLARISA read-only via HTTP and caches each catalog in its own MySQL table, refreshed by a scheduled cron (
clarisaCron.service.ts). The frontend always reads catalog data through /clarisa/* — never directly from CLARISA. Syncs must be idempotent: re-running leaves the cache identical.Theory of Change services
Theory of Change services
The
toc/ module fetches ToC trees and outcomes from an external Theory of Change service. PRMS uses these to let submitters align results to ToC outcome nodes. PRMS reads ToC data — it does not author or govern ToC content.AWS Cognito + Active Directory
AWS Cognito + Active Directory
Authentication flows through AWS Cognito combined with Active Directory (LDAP via
ldapts). PRMS issues and verifies JWTs server-side but does not own identity provisioning. User records in PRMS mirror AD accounts via the api/ad_users/ module.CGSpace
CGSpace
Knowledge products reference a stable
handle from CGSpace, the CGIAR institutional repository. PRMS stores the handle and links out — it does not host PDFs or datasets.MQAP
MQAP
The
api/m-qap/ module calls the MQAP service to pre-fill knowledge product attributes (e.g., journal metrics, publication details) based on a CGSpace handle or DOI.AWS S3 and SharePoint
AWS S3 and SharePoint
RabbitMQ
RabbitMQ
The
reporting-metadata-export pipeline uses RabbitMQ for async processing. The consumer (api/results/reporting-metadata-export.consumer.ts) acknowledges messages only after successful processing. Failed messages are re-queued for retry rather than dropped.Pusher and WebSockets
Pusher and WebSockets
Real-time events — new QA assignments, submission status changes, share requests — are published via the Pusher server SDK and consumed by the Angular client through
pusher-js and ngx-socket-io.DynamoDB (logs only)
DynamoDB (logs only)
Operational log events are written to DynamoDB via
dynamoose and surfaced at /logs/*. DynamoDB is not used for any business entity — MySQL is the system of record.Role model and authorization
PRMS uses a numeric role hierarchy. Lower numeric values indicate higher privilege. Roles are scoped by type:Initiative, Action_Area, or Application.
| Role | Numeric value | Scope |
|---|---|---|
| Admin | 1 | Application |
| Guest | 2 | Initiative / Application |
| Lead | 3 | Initiative |
| Co-Lead | 4 | Initiative |
| Coordinator | 5 | Initiative |
| Member | 6 | Initiative |
| Action Area Global Director | 7 | Action Area |
| Action Area Coordinator | 8 | Action Area |
ValidRoleGuard with the @Roles(role, type) decorator. A user is authorized when their role value is less than or equal to the required role value (i.e., having a higher-privilege role grants access to lower-privilege endpoints). Frontend role gates are UX-only — the backend always enforces.
Admin endpoints (phase management, global parameters, user management, delete-recover, CLARISA sync) require
RoleEnum.ADMIN = 1. Initiative-level operations (creating results, requesting QA) require at minimum RoleEnum.MEMBER = 6 scoped to the relevant initiative.Key design decisions
Custom auth header, not Authorization: Bearer
Custom auth header, not Authorization: Bearer
Bilateral and platform-report routes are JWT-excluded
Bilateral and platform-report routes are JWT-excluded
/api/bilateral/* and /api/platform-report/* do not require a JWT. These are headless surfaces for bilateral funders and platform-report consumers that cannot send an auth header in their integration context. Protection for these routes is handled at the network perimeter (IP allowlist, API Gateway authorization) rather than in the NestJS application.MySQL is the system of record; DynamoDB is logs-only
MySQL is the system of record; DynamoDB is logs-only
All business entities — results, roles, phases, CLARISA caches, review history — live in MySQL. DynamoDB holds operational log events only. This keeps schema management, migrations, and transactional integrity centralized in a single relational database.
Soft delete, never hard delete
Soft delete, never hard delete
Deleting a result, evidence row, partner, or ToC link sets
is_active = false and writes an audit row. Hard deletion is admin-only and rare. The api/delete-recover-data/ module lets admins recover soft-deleted results without manual SQL.CLARISA is always read-only from PRMS
CLARISA is always read-only from PRMS
PRMS never writes back to CLARISA. The catalog caches in MySQL are populated by the CLARISA cron and can be refreshed on-demand by platform admins. All catalog reads in the frontend go through
/clarisa/* — the Angular client never calls CLARISA directly.