Data Flow Diagrams

Context and levelled process diagrams for how applicants, students, lecturers, and staff exchange data with Portal API, the existing CMS, planned Postgres, and the evidence file volume.

Gane & Sarson style via Mermaid Stage 1: mock + stub CMS CAP-53 = anti-corruption layer Target flows marked where CAP-53 applies
Clean canonical inside; ugly legacy translation outside. CAP-53 is not a 1:1 DB adapter — it is the CMS anti-corruption layer (mapping, validation, idempotent ops, reconciliation). Full guidance: LUCT CMS / Portal compatibility.
Level 0 — ContextWhole system boundary Level 1 — Portal systemMajor processes & stores CMS ACL (CAP-53)Anti-corruption layer Auth & sessionIdentity → person → RBAC Admissions (M2)Apply → offer → enrol Registration & feesModules → invoice · workflow Teaching & resultsProgress service · publication Stage 1 runtimeWhat runs today Deploy pathTraefik → k3s → stores
External entity Square / actor outside the system boundary.
Process Rounded node — transforms or routes data.
Data store Cylinder — durable or session state.
Data flow Labeled arrow — what moves, not how.

Level 0 — Context diagram

The Raisd portal control plane as a single process.

0. Portal Control Plane

Four role populations + IdP; three persistence backends.

flowchart TB
  A(["Applicant"])
  S(["Student"])
  L(["Lecturer"])
  T(["Staff / Registry / Bursary"])
  IDP(["Identity Provider / CMS Login"])

  P0(("0
Raisd Portal
Control Plane")) CMS[("D1 Existing CMS
System of Record")] PG[("D2 Postgres
planned")] FS[("D3 Evidence / file volume")] MAIL(["UltaCP Mail
notifications only"]) A -->|"application, evidence, accept offer"| P0 P0 -->|"status, offer, enrolment ack"| A S -->|"registration, payments, submissions, forms"| P0 P0 -->|"timetable, invoices, results, materials"| S L -->|"materials, attendance, marks, messages"| P0 P0 -->|"class lists, submissions, published grades"| L T -->|"review, allocate, publish, configure"| P0 P0 -->|"queues, ledgers, case status"| T IDP -->|"auth assertion / session subject"| P0 P0 <-->|"enrolment, marks, ledger, applications"| CMS P0 <-->|"portal-owned durable state"| PG P0 <-->|"file bytes + metadata refs"| FS P0 -.->|"outbound notices"| MAIL
Portals never connect to D1–D3 directly. Mail is out-of-band (not on the request path). Staff continue to use the existing CMS UI where it already works; new staff-portal screens only fill verified gaps.

Level 1 — Portal system decomposition

Major processes inside the control plane boundary.

1.x processes

Web shells → Portal API → domain handlers → stores / CMS anti-corruption layer.

flowchart TB
  subgraph External
    A(["Applicants"])
    S(["Students"])
    L(["Lecturers"])
    T(["Staff"])
    IDP(["IdP / CMS login"])
  end

  subgraph ControlPlane["Control plane"]
    P1(("1.1
Role portal
web shells")) P2(("1.2
Auth &
session")) P3(("1.3
Portal API
RPC gateway")) P4(("1.4
Admissions
handler")) P5(("1.5
Academic &
registration")) P6(("1.6
Finance
handler")) P7(("1.7
Teaching &
assessment")) P8(("1.8
Forms / immigration
/ graduation")) P9(("1.9
CMS ACL
CAP-53")) end subgraph Stores DS[("D0 Session store
in-memory today")] CMS[("D1 Existing CMS")] PG[("D2 Postgres planned")] FS[("D3 Evidence files")] MOCK[("D4 Mock PortalRecordGraph
demo fixtures")] end A --> P1 S --> P1 L --> P1 T --> P1 P1 -->|"HTTPS UI calls"| P3 IDP --> P2 P2 -->|"token / subject"| P3 P2 <--> DS P3 --> P4 P3 --> P5 P3 --> P6 P3 --> P7 P3 --> P8 P4 --> P9 P5 --> P9 P6 --> P9 P7 --> P9 P8 --> P9 P4 <--> FS P6 <--> FS P7 <--> FS P8 <--> FS P9 <--> CMS P4 -.-> PG P5 -.-> PG P6 -.-> PG P3 -.->|"stage 1"| MOCK

Process catalogue

What each Level-1 process owns.

IDProcessInputsOutputs / side effects
1.1 Role portal webs User actions in apply / student / teach / staff HTTPS to Portal API; rendered views
1.2 Auth & session Credentials / IdP assertion Bearer token; session record (TTL 12h today)
1.3 Portal API RPC POST /v1/portal/:method + bearer { result, cms } envelope
1.4 Admissions Application drafts, evidence files, acceptances CMS application / offer / first enrolment
1.5 Academic & registration Module selections, credit overrides Registrations, study-plan views
1.6 Finance Invoice queries, payment proofs Allocations (bursary), funding awards
1.7 Teaching & assessment Materials, attendance, marks, submissions Published results (student-visible)
1.8 Forms / immigration / graduation Form answers, case docs Case status, linked invoices
1.9 CMS anti-corruption layer Canonical read/write intents; never raw SQL from handlers Legacy↔canonical map, validation, idempotent CMS ops, DQ, reconciliation

CMS Anti-Corruption Layer (CAP-53)

Domain handlers never contain arbitrary legacy SQL. Example: getCurrentStudyPeriod(student) may require walking r_student → r_stdprogram → r_stdsemester → f_term → r_stdmodule then returning a canonical StudyPeriod.

Mapping · Validation · Operations

Full responsibility list on the architecture page.

flowchart TB
  H(("Domain handler"))
  ACL(("1.9 CMS ACL"))
  MAP[LegacyEntityMap]
  DQ[DataQualityIssue]
  IOP[IntegrationOperation]
  CMS[(Existing CMS)]
  REC[Reconciliation reports]

  H -->|"canonical command / query"| ACL
  ACL --> MAP
  ACL --> DQ
  ACL --> IOP
  ACL <-->|"translated ops"| CMS
  ACL --> REC
          
Writes are idempotent (idempotency key). Campus schema differences use CampusCmsAdapter behind the same canonical interface. One owner per domain — see SoR table.

Auth & session (current + target)

Stage 1 uses stub login and an in-memory session map.

Current Stage 1 path

POST /v1/auth/login → UUID token → every portal method.

sequenceDiagram
  autonumber
  actor U as Browser (role portal)
  participant API as Portal API
  participant SS as D0 Session store
  participant Mock as D4 MockPortalApi

  U->>API: POST /v1/auth/login { scenarioId, ... }
  API->>SS: create Session(token, TTL 12h, scenario)
  API-->>U: { token }
  U->>API: POST /v1/portal/{method}
Authorization: Bearer token API->>SS: lookup token SS-->>API: session + scenario + studentProfileId API->>Mock: invoke PortalApi method Mock-->>API: result API-->>U: { result, cms: { acknowledged, mode: stub } }

Target auth — identity mapping

IdP subject → Account → Person → Role/Scope. Legacy password/email login stays behind a temporary adapter — not canonical design. RBAC scopes: role × campus × faculty × programme × module offering × operation.

flowchart LR
  U(["User"])
  IDP(["IdP / CMS login"])
  P2(("1.2 Auth"))
  DS[("Session / token store")]
  P3(("1.3 Portal API"))
  RBAC{{"RBAC
role × campus × faculty
× programme × offering × op"}} H(("Domain handler")) U -->|"login"| IDP IDP -->|"stable subject"| P2 P2 -->|"Person / Student / Staff map"| DS U -->|"Bearer + method"| P3 P3 --> DS P3 --> RBAC RBAC --> H

Admissions data flow (Milestone 2 target)

Applicant portal → evidence volume + CMS application lifecycle.

Apply → review → offer → first enrolment

Registry reviews inside existing CMS; portals surface status.

flowchart TB
  A(["Applicant"])
  REG(["Registry staff"])

  P1(("1.1 Applicant portal"))
  P3(("1.3 Portal API"))
  P4(("1.4 Admissions"))
  P9(("1.9 CMS ACL"))

  FS[("D3 Evidence volume")]
  CMS[("D1 CMS applications / offers")]
  STU[("D1 / D2 Student + ProgrammeEnrolment")]

  A -->|"draft application"| P1 --> P3 --> P4
  A -->|"upload evidence file"| P1 --> P3 --> P4
  P4 -->|"store bytes"| FS
  P4 -->|"file id + checksum + type"| P9 --> CMS

  A -->|"submit application"| P1 --> P3 --> P4 --> P9 --> CMS

  REG -->|"review in CMS UI"| CMS
  CMS -->|"status / offer"| P9 --> P4 --> P3 --> P1 --> A

  A -->|"accept offer"| P1 --> P3 --> P4 --> P9
  P9 -->|"create first enrolment"| STU
  P9 -->|"bind student identity"| CMS
  P4 -->|"enrolment ack"| P3 --> P1 --> A
          
Evidence object metadata (type, checksum, uploadedAt) is stored with the application; raw bytes stay on D3. Declarations capture policy version + acceptedAt.

Registration & fees (Milestone 3 target)

Module selection creates CMS registration; invoices and payment proofs follow. Define transaction / workflow states so UI success is not a partial write.

Register modules → invoice → proof → allocate

Proof ≠ payment: bursary verifies and writes ledger Payment / Allocation.

flowchart TB
  S(["Student"])
  BUR(["Bursary / Finance staff"])

  P5(("1.5 Academic & registration"))
  P6(("1.6 Finance"))
  P9(("1.9 CMS ACL"))

  CMS_R[("D1 Module registrations")]
  CMS_F[("D1 Finance ledger")]
  FS[("D3 Payment proof files")]

  S -->|"select modules / submit registration"| P5
  P5 -->|"validate credits, prereqs, window"| P5
  P5 -->|"registration transaction"| P9 --> CMS_R
  P9 -->|"fee lines from FeePlan"| P6
  P6 -->|"issue Invoice INV-######"| P9 --> CMS_F
  P6 -->|"invoice view"| S

  S -->|"upload payment proof
amount, bank ref, file"| P6 P6 -->|"store proof file"| FS P6 -->|"proof metadata queue"| P9 --> CMS_F BUR -->|"verify proof in CMS / staff portal"| CMS_F BUR -->|"record Payment + Allocation"| P9 P9 --> CMS_F P6 -->|"receipt / updated balance"| S

Finance data stores detail

What moves between student UI and ledger entities.

flowchart LR
  subgraph Student_visible
    IV[Invoice + lines + tax]
    PP[Payment proof upload]
    BA[Balance / account issues]
  end

  subgraph Ledger_SoR
    FA[FinanceAccount]
    PAY[Payment]
    ALL[PaymentAllocation]
    ADJ[FinancialAdjustment]
    FUND[FundingAward / Benefit]
  end

  IV --- FA
  PP -.->|"staff confirms"| PAY
  PAY --> ALL
  ALL --> IV
  FUND --> ADJ
  ADJ --> FA
  BA --- FA
          

Registration → invoice workflow states

If registration succeeds and invoice fails, status must not report UI “success”. Use idempotency keys on retries.

flowchart LR
  P[PENDING] --> PR[PROCESSING]
  PR --> C[CMS_COMMITTED]
  C --> F[FINANCE_PENDING]
  F --> OK[COMPLETED]
  PR --> FAIL[FAILED]
  C --> REC[REQUIRES_RECONCILIATION]
  F --> REC
          

Teaching, submissions & results

Lecturer publishes materials; student submits; marks publish only when released. GPA/CGPA/credits come from AcademicProgressService; ResultPublication versions official history.

Materials → submit → mark → publish

Students see published results only. ResultAccessPolicy may gate visibility (funding/balance/holds).

flowchart TB
  L(["Lecturer"])
  S(["Student"])
  REG(["Registry"])

  P7(("1.7 Teaching & assessment"))
  P9(("1.9 CMS ACL"))
  FS[("D3 Learning / submission files")]
  CMS[("D1 Marks & attendance SoR")]

  L -->|"publish ModuleLearningItem / Assessment"| P7
  P7 -->|"file bytes"| FS
  P7 -->|"metadata"| P9 --> CMS

  S -->|"download materials"| P7 --> FS
  S -->|"AssignmentSubmission"| P7
  P7 -->|"submission file"| FS
  P7 -->|"submission metadata"| P9 --> CMS

  L -->|"record Attendance"| P7 --> P9 --> CMS
  L -->|"enter AssessmentResult"| P7 --> P9 --> CMS

  REG -->|"publish ModuleResult / TermResult"| P9 --> CMS
  CMS -->|"publishedAt set"| P7
  P7 -->|"results visible"| S
          
Soft-delete on submissions uses deletedAt. Learning progress statuses: not-started | viewed | completed | locked.

Stage 1 runtime (what actually runs today)

portal-api is an HTTP façade over the student-portal mock engine.

As-built Stage 1

No live CMS adapter; cms.mode = stub on every response.

flowchart LR
  SP[Student portal
Vite app] API[portal-api
Fastify] SS[("In-memory
Session Map")] MOCK[MockPortalApi
from student-portal] GRAPH[("PortalRecordGraph
fixture data")] SP -->|"VITE_PORTAL_API_URL"| API API <--> SS API --> MOCK MOCK <--> GRAPH API -->|"cms: stub ack"| SP
Demo or “frontend ready” is not Live. Live requires real CMS data, durable save, server-enforced permission, and a paired staff/lecturer path (product rule).

API surface domains

OpenAPI lists ~55 PortalApi methods under these groups.

flowchart TB
  GW(("POST /v1/portal/:method"))

  GW --> I[immigration]
  GW --> F[online-forms]
  GW --> A[announcements / policies]
  GW --> R[lecturer-review]
  GW --> D[dashboard]
  GW --> AC[academic / registration / materials]
  GW --> PR[profile / services]
  GW --> C[community / calendar / chat]
  GW --> FI[finance]
  GW --> G[getPortalRecordGraph]
          

Deployment data path (SDD-12)

Browser → Traefik → portal Deployments on k3s / UltaHost Singapore.

Ingress to stores

Hostnames such as apply.raisd.co / api.raisd.co.

flowchart LR
  B(["Browser"])
  TR[Traefik ingress]
  APPLY[apply Deployment]
  STUD[student Deployment]
  TEACH[teach Deployment]
  STAFF[staff Deployment]
  API[portal-api Deployment]
  PG[("Postgres StatefulSet
planned")] PVC[("PVC evidence")] CMS[("Existing CMS
UltaHost Premium")] B --> TR TR --> APPLY TR --> STUD TR --> TEACH TR --> STAFF TR --> API APPLY --> API STUD --> API TEACH --> API STAFF --> API API --> PG API --> PVC API --> CMS

Cross-cutting flows

Online forms, immigration, graduation

Shared pattern: definition → submission → messages → optional invoice / case.

flowchart TB
  U(["Student"])
  ST(["Staff desk"])
  P8(("1.8 Forms / immigration / graduation"))
  FS[("D3 Attachments")]
  CMS[("D1 Case / form SoR")]
  INV[("Invoice if fee configured")]

  U -->|"start submission"| P8 --> CMS
  U -->|"answers + files"| P8
  P8 --> FS
  P8 --> CMS
  ST -->|"message / request changes"| P8 --> CMS
  P8 -->|"thread"| U
  P8 -->|"open ImmigrationCase / GraduationRecord"| CMS
  P8 -.->|"fee from campus config"| INV