Architecture review — LUCT CMS / Portal compatibility
Pilot project: Lesotho (Working Group, 3 Oct 2026) — first acceptance campus; newer CMS screenshots are the feature benchmark; see Lesotho feature analysis and lesotho-pilot.md. Guidance also incorporates Cyberjaya LUCT CMS and Student Portal operational experience. Keep the canonical domain clean; concentrate legacy complexity in the CMS anti-corruption boundary (CAP-53). This phase (confirmed 23 Sep 2026; pilot campus updated 3 Oct): existing campus CMS family remains System of Record; portals via Portal API; do not duplicate operational desks. Longer term: progressive unified one-stop CMS on verified gaps + migration strategy (target delivery). Ideal (proposed): one product, distributed country databases, white-label and feature flags. Portals never talk to CMS / Postgres / files directly.
Software Design Documents and ADRs
Product baseline for Working Group CMS 2026. Hub: sdd/ · Nomenclature: SDD-15.
Architecture Decision Records
| ID | Decision | Summary | Recorded in |
|---|---|---|---|
| ADR-1 | Existing CMS as initial backend | Campus CMS family remains SoR this phase; portals via Portal API; do not duplicate operational desks. Pilot acceptance campus = Lesotho (3 Oct 2026). Progressive unified CMS is M5+ on verified gaps. | SDD-02 |
| ADR-2 | Singapore, on UltaHost | Host on the existing UltaHost VDS estate in Singapore. Machine size follows the launch stage, not the largest machine already owned. | SDD-12 |
| ADR-3 | Kubernetes, starting at the minimum | Application workloads on k3s. Existing CMS and mail stay off Kubernetes. Start single-node; grow by stage. | SDD-12 |
Software Design Documents
| ID | Document | What it covers |
|---|---|---|
| SDD-00 | Document control | Status, sources, glossary, status meanings |
| SDD-01 | System overview | Product intent, portals, constraints, current vs target |
| SDD-02 | Architecture and integration | Existing CMS as backend, Portal API, auth, mobile, data |
| SDD-03 | Delivery milestones | Admissions-first sequence and release conditions (M1–M5) |
| SDD-04 | Applicant portal | Apply → evidence → track → offer → enrol |
| SDD-05 | Student portal | Learning, records, fees, support (current v1 frontend) |
| SDD-06 | Lecturer portal | Publish, attendance, marking, class communication |
| SDD-07 | Admin / staff CMS | Registry, Faculty, Bursary, QA, Marketing, remaining desks |
| SDD-08 | Shared platform | Integration spine, security, ops, training |
| SDD-09 | Requirements traceability | Product decisions, campus requests, country evidence, UI baseline |
| SDD-10 | Open questions | Decisions still required before Live |
| SDD-11 | Capability catalogue | All 112 CAP rows from the detailed checklist |
| SDD-12 | Deployment architecture | Repositories, VDS hosts, public names, request path |
| SDD-13 | Progress and timeline | Latest sheet snapshot, completion, sequence, remaining tasks |
| SDD-14 | Existing CMS vs Raisd portals | Cyberjaya LUCT CMS inventory mapped to CAP IDs |
| SDD-15 | Nomenclature | Quick links for every CAP-* and M1–M5 |
Six instructions for documentation authors
- Keep the canonical academic model clean — do not copy the legacy schema.
- Upgrade CAP-53 from “CMS database adapter” to a documented anti-corruption / integration layer.
- Make historical academic state immutable/auditable and calculations reproducible.
- Explicitly model statuses, curriculum versions, repeats, transfers, exemptions, and credit/CGPA rules — do not hide them in SQL.
- One authoritative owner per domain; cross-system operations must be idempotent, recoverable, and reconcilable.
- Assume legacy data is imperfect — detect and expose inconsistencies rather than silently guessing.
Suggested target architecture
Clean domain inside; legacy translation outside. Incremental migration — not a big-bang CMS rewrite.
Portals → Portal API → domains → ACL → CMS
Staff keep using existing CMS UI where it works; new staff screens fill verified gaps only.
flowchart TB
subgraph Portals
ST[Student]
LE[Lecturer]
SF[Staff]
AP[Applicant]
end
API[Portal API]
subgraph Domains
AC[Academic domain]
FI[Finance domain]
AD[Admissions domain]
end
CAN[(Canonical domain model)]
PG[(Postgres owned data)]
subgraph ACL["CMS Anti-Corruption Layer CAP-53"]
MAP[Mapping]
VAL[Validation]
OPS[Operations]
end
CMS[(Existing CMS SoR)]
ST --> API
LE --> API
SF --> API
AP --> API
API --> AC
API --> FI
API --> AD
AC --> CAN
FI --> CAN
AD --> CAN
CAN --> PG
CAN --> ACL
MAP --> CMS
VAL --> CMS
OPS --> CMS
Core academic path (keep this readable)
Developers must understand this without opening legacy SQL.
flowchart TB
SP[StudentProfile] --> PE[ProgrammeEnrolment]
PE --> PV[ProgrammeVersion]
PV --> CM[CurriculumModules]
PE --> STU[StudyPeriod]
STU --> MR[ModuleRegistration]
MR --> RES[ModuleResult]
MOD[Module] --> MO[ModuleOffering]
MO --> CAM[Campus]
MO --> AT[AcademicTerm]
MO --> TA[TeachingAssignment]
PV --> APS[AcademicProgressService]
CM --> APS
MR --> APS
RES --> APS
APS --> TR[TermResult / Progress snapshots]
TR --> PUB[ResultPublication]
PUB --> TX[Transcript presentation]
CMS Adapter = Anti-Corruption Layer (CAP-53)
Not a 1:1 table access layer. Legacy meaning lives across tables, views, stored procedures, and old PHP. Domain handlers must not embed arbitrary legacy SQL.
Responsibilities
Example: getCurrentStudyPeriod(student) may walk
r_student → r_stdprogram → r_stdsemester → f_term → r_stdmodule
and return a canonical StudyPeriod.
| Responsibility | Intent |
|---|---|
| Translate legacy → canonical | CMS records become domain entities |
| Translate commands → CMS ops | Domain writes become required CMS operations |
| Resolve legacy status meanings | Map ambiguous ACTIVE / registered / current flags |
| Map legacy IDs ↔ canonical IDs | Via LegacyEntityMap — never reuse CMS IDs as PK |
| Handle campus/schema differences | CampusCmsAdapter where schemas differ |
| Validate inconsistent legacy records | Raise DataQualityIssue, do not silently guess |
| Safe transactional writes | Defined boundaries; no partial “success” |
| Idempotent writes | IntegrationOperation + idempotency key |
| Report mapping / sync failures | Surface to ops and reconciliation |
| Support reconciliation | Counts and domain integrity checks |
CMS vs Postgres — one authority per domain
Never leave “CMS says ACTIVE / Postgres says DEFERRED” without a winner.
| Domain | Current authority | Future authority |
|---|---|---|
| Student identity | CMS | Migration decision TBD |
| Programme enrolment | CMS | TBD |
| Study periods | CMS | TBD |
| Module registration | CMS | TBD |
| Official results | CMS | TBD |
| Finance ledger | CMS | TBD |
| Application portal state | Decide explicitly (Portal/Postgres or CMS) | Postgres/CMS |
| Portal session | Portal | Portal |
| Uploaded file bytes | File store | File store |
| Legacy ID maps / DQ issues / audit | Portal Postgres (planned) | Portal Postgres |
Academic model decisions
Keep ProgrammeVersion
Enrolment is pinned to the version under which the student enrolled (e.g. BBA 129 vs 122 credits). Do not resolve requirements from “current” programme config.
ProgrammeCampusOffering (preferred over Faculty-as-campus)
Same programme may be delivered at several campuses while sharing academic identity. Faculty remains organisational; offering is explicit.
flowchart LR
P[Programme] --> PV[ProgrammeVersion]
PV --> PCO[ProgrammeCampusOffering]
PCO --> B[Botswana]
PCO --> U[Uganda]
PCO --> S[Sierra Leone]
ModuleOffering (module × term × campus). See
ERD curriculum.
ProgrammeEnrolment.campusId remains the student’s study campus.
Credits earned ≠ credits in CGPA
Retain ModuleResult.includedInCgpa. Pass/elective/exemption cases must be representable without special-case transcript SQL.
Valid example: Grade PASS · Credits earned 3 · CGPA points 0 · Included in CGPA false.
Also support: normal, repeat, exemption, credit transfer, pass/fail, audit/non-credit, replacement, additional elective, transferred programme credit — via registration type + result flags, not ad-hoc SQL.
Calculations are reproducible snapshots
Authoritative inputs: ModuleRegistration + ModuleResult + CurriculumModule + ProgrammeVersion + grading rules. TermResult is a calculated snapshot (calculationVersion, calculatedAt), not primary truth.
ResultPublication versions published academic history
Publication is not only publishedAt on the current row. Corrections create a new publication that supersedes the previous; prior versions remain auditable.
AcademicTerm ≠ student’s programme semester
Keep explicit: calendar AcademicTerm (e.g. 2026-03) vs StudyPeriod.programmeSemesterNumber (Semester 5 for one student, Semester 3 for another).
Attempts are never overwritten
ModuleRegistration.attemptNumber + registrationType. Failed then repeat keeps both rows; policy decides CGPA/credit effect.
AcademicProgressService — policy not transcript SQL
calculateModuleOutcome, credits, term GPA, CGPA, graduation progress, prerequisites. Transcript is a presentation of calculated history. One service — not portal/registry/transcript/stored-proc each inventing rules.
History is never rewritten
Defer / return / transfer / withdraw / re-enrol create or transition records. Use EnrolmentStatusHistory (and similar) for auditability.
Define “active student” properly
EMGS active ≠ CMS registered ≠ current semester ≠ current active report. Avoid one universal student.status = ACTIVE.
| Layer | Status lives on |
|---|---|
| Identity | StudentProfile |
| Enrolment | ProgrammeEnrolment |
| Academic period | StudyPeriod |
| Registration | ModuleRegistration |
| Immigration | StudentVisaPass / ImmigrationCase |
| Finance | FinanceAccount / account issues |
Named queries: isCurrentlyEnrolled(), isAcademicallyActive(),
hasCurrentStudyPeriod(), isRegisteredForModules(),
hasActiveImmigrationStatus(), isEligibleForStudentPortal().
Every report must state which definition it uses.
Demo draft of the cohort definition and open Registry questions:
canonical schema confirmations
(CS-01–CS-03; Schema v3 policy Demo seeded).
Integration operations
LegacyEntityMap
Canonical UUID ↔ sourceCampus + legacyTable + legacyId. Supports overlapping numeric IDs across campuses.
DataQualityIssue + reconciliation
Expect imperfect legacy data. Flag CURRENT_ENROLMENT_WITHOUT_STUDY_PERIOD, LEGACY_PASSPORT_MISMATCH, etc. CAP-53 reports CMS vs mapped counts and domain integrity checks.
Idempotency — IntegrationOperation
Register module, accept offer, create enrolment, submit marks, publish results, invoice, payment, forms — all keyed. Retries must not double-write.
Transaction / workflow states
Registration → CMS → invoice must define failure modes. UI success only when the agreed boundary completes.
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
AuditEvent
Who / what / when / why / before / after for marks, grades, registrations, enrolments, finance, publications, visa — not only application logs.
Migration: preserve raw legacy
Legacy source → raw import → mapping/validation → canonical. Never silently “fix” historical academic data on import.
Campus differences = configuration
Effective-dated campus policy tables (Schema v3 Demo: status mapping, academic-status, credit, CGPA, document numbering, adjustments) plus CampusCmsAdapter. Canonical model stays consistent; no if (campus === "botswana") scattered in domain services.
Identity ≠ legacy password behaviour
IdP → stable subject → Person/Student/Staff mapping. Legacy password/email login stays behind a temporary adapter — not canonical design.
RBAC scopes
Beyond role × method × campus: faculty, programme, module offering, operation. A Botswana lecturer does not automatically see every Botswana student’s marks.
ResultAccessPolicy
Rule-based (campus, funding, balance, holds, term) → allowed + reasonCode. Not hardcoded if (balance < 1000) in the portal.
Integrity constraints (document intentionally)
Examples: unique studentNumber where applicable; unique StudyPeriod(enrolment, term); ModuleResult 1:1 registration; TermResult 1:1 study period; controlled registration uniqueness by attempt; payment allocation limits; offer accept must not create two first enrolments.
Integration health
Beyond API/DB up: adapter latency, failed writes, retry queue, unmapped records, reconciliation mismatches, stuck registrations, file/metadata orphans.
Documentation priority
| Priority | Change |
|---|---|
| Critical | CMS Adapter as anti-corruption layer |
| Critical | CMS/Postgres source-of-truth ownership |
| Critical | Legacy-ID mapping |
| Critical | Transaction + retry/idempotency behaviour |
| Critical | Deterministic academic calculation rules |
| Critical | Explicit active/enrolled/registered statuses |
| High | Result/calculation versioning |
| High | Audit trail |
| High | Migration reconciliation / data quality |
| High | Programme campus offering independently |
| High | Repeats / exemptions / transfers / pass-fail |
| High | Campus-specific configuration strategy |
| Medium | Integration monitoring |
| Medium | Result-access / finance policy |
| Medium | RBAC scopes beyond campus |
| Later | Fully retire legacy CMS procedures/views |