SAMA · SPP Learning HUB

Architecture & Data-Flow

Every service, every table, every flow, and the security controls between them. This is the reference every reader (engineer, auditor, PM) can open once and understand the whole platform without touching the code.

System overview

Four tiers, left to right: viewers, application, Supabase platform, and persistence. Every request is served by the React SPA, is authorized by GoTrue and PostgREST, and lands on Postgres with Row Level Security enforced on every read and write.

Viewers Application Supabase Platform Persistence Trainees · 100 Email + password login /user/* Staff · 45 Admin, Mentor, LM, ... MFA required Partners · 10 External agencies MFA required SAMA Reviewers Audit + Reports SAMA HR Credential handover (out-of-band) React SPA Vite build · CDN React Router + code-split Supabase JS SDK MfaGuard · useIdleTimeout DOMPurify (article HTML) JWT on every request 15 min idle timeout Resend Email delivery API via DE-NET infra Auth · GoTrue /auth/v1 · password + TOTP · JWT aal1↔aal2 PostgREST /rest/v1 · auto REST · RLS every request Realtime · WebSocket Postgres logical replication · chat + notifications Storage API S3-compat · presigned · per-object RLS Edge Functions · Deno drain_email_outbox · notify_cron grade_assessment · upload-scan Postgres KMS-encrypted at rest profiles · roles · cohorts contents · assessments capstone_* · rotation_* surveys · nudges · email_outbox RLS on every table Column-lock triggers Daily snapshot · WAL 7d PITR Object Storage S3-compat content-files · cohort-files mentor-photos · team-chat capstone-submissions rotation-evidence Versioned · soft-delete 30d auth.* schema users · identities · sessions refresh_tokens mfa_factors · mfa_challenges bcrypt · TOTP secret

How to read the diagram

  • Left to right is the request path: a user in a browser hits the SPA, which calls the Supabase services, which read and write Postgres.
  • Dashed brown arrows are asynchronous / out-of-band data movements (HR credential handover).
  • Purple boxes are Supabase managed services. Every one of them applies Row Level Security on the caller's JWT before touching Postgres.
  • Gold boxes are external systems out of SAMA's platform boundary (Resend for email delivery via DE-NET infra, SAMA HR).

End-to-end data flows

Pick a flow. Each one traces the actor through the tiers with numbered steps that map directly to source code (routes, RLS policies, RPCs, tables). Use these as the input for any Request Sequence diagram.

Tier deep-dive

Every tier has a clear responsibility and a hard boundary with the next. Here is what each tier owns, its inputs, its outputs, and the security controls it enforces.

Tier 1 · Client (React SPA)

The browser-side application. Ships as static assets from a CDN.
Runtime
React 18 · Vite · React Router · Tailwind
Bundle strategy
Code-split by portal (Trainee ships eagerly; Admin, Mentor, LM, Partner, Facilitator, Reports lazy)
Auth SDK
Supabase JS · Bearer JWT on every fetch
Cross-cutting
MfaGuard · useIdleTimeout (15 min · media-heartbeat aware) · useForcePasswordChange · DOMPurify sanitization
Storage
JWT + refresh token in localStorage (Supabase default) · no PII cached
Static hosting
CDN of the Vite build; no server-side rendering
Fails closed
If MFA cannot be evaluated, gate routes to enrollment rather than passing through

Tier 2 · Application (Supabase services)

Five services collocated behind the gateway. Each one authorizes independently.
ServicePrefixResponsibilitySecurity
GoTrue Auth /auth/v1 Password sign-in, MFA enroll/challenge/verify, refresh, password reset bcrypt hash · TOTP · JWT aal1↔aal2 claim
PostgREST /rest/v1 Auto-generated REST over the public schema RLS applied to every SELECT / INSERT / UPDATE / DELETE
Realtime /realtime/v1/websocket Live broadcast of Postgres logical replication events RLS applied on subscribe · per-user filter
Storage API /storage/v1 S3-compatible file upload / download / list RLS on storage.objects · owner-scoped · MIME allow list · per-bucket policies
Edge Functions /functions/v1 Deno-based async jobs (email drain, grading, cron, AI proxy) Service-role key held server-side · never in the browser

Tier 3 · Persistence (Postgres + Storage + auth.*)

Where the data actually lives. Encrypted at rest. RLS enabled everywhere in public.

Postgres · public schema

  • Catalog: tracks · cohorts · modules · topics · contents · assessments · surveys · roles · permissions
  • Workflow: rotation_plans · rotation_months · rotation_month_stages · annual_leave_windows · annual_leave_requests · capstone_teams · capstone_submissions · capstone_submission_reviews · capstone_escalations
  • People: profiles · cohort_members · mentor_trainees · mentor_bookings · partner_assignments · line_manager_trainees
  • Communication: team_chat_messages · notifications · nudges · email_outbox · email_templates
  • Audit: activity_log · mfa_events

auth.* schema

  • auth.users · bcrypt password hash
  • auth.identities · sessions · refresh_tokens
  • auth.mfa_factors · mfa_challenges · mfa_amr_claims

Object storage · 6 buckets

BucketPurposePolicy
content-filesCourse PDF / video / PPT / DOCXOwner-scoped write · staff bypass · MIME allow list (no SVG)
cohort-filesCohort docsOwner-scoped write · staff bypass
mentor-photosMentor profile picturesOwner-scoped · authenticated read
capstone-submissionsTeam deliverablesTeam-scoped read · facilitator/staff bypass
rotation-evidenceOJT stage evidenceOwner-scoped · assigned Line Manager can review
team-chatChat attachmentsTeam-scoped read

Portals & roles — request sequences

One table per portal. Each row is a common user action broken into its request sequence: what the user does → what the SPA calls → what data is touched → which policy allows it → what the user sees back.

Trainee portal · /user/*

100 accounts · role learner · email + password login · MFA optional
User action SPA call Data touched Policy / trigger Response
Sign in POST /auth/v1/token?grant_type=password auth.users bcrypt verify · JWT aal1 Routed to /welcome/user
Open a content item GET /rest/v1/contents?id=eq.X contents · content_slides contents_read_all Viewer renders (DOMPurify sanitized for articles)
Mark content complete UPSERT /rest/v1/content_progress content_progress · module_progress owner scope user_id = auth.uid() Progress bar advances
Take a quiz INSERT assessment_attempts · INSERT assessment_answers assessment_questions_public (view) · assessment_attempts attempts_guard_grading zeroes score Staff grades later; trainee sees final score
Upload assignment POST /storage/v1/object/content-files storage.objects · assignment_submissions owner-scoped insert · staff bypass File visible in submission list
Submit leave request INSERT annual_leave_requests annual_leave_requests owner insert · status locked by leave_guard_status Row visible in "My leave" with status submitted
Upload OJT evidence POST rotation-evidence · UPDATE rotation_month_stages rotation_month_stages.evidence_url owner UPDATE (non-completion cols) Evidence visible to the assigned Line Manager
Book mentor session INSERT mentor_bookings mentor_slots · mentor_bookings trainee_id = auth.uid() · slot unique Mentor notified via notifications
Send team chat message INSERT team_chat_messages team_chat_messages chat_team_insert: member + author self Realtime WS pushes to teammates

Admin portal · /admin/*

Role admin / super_admin · MFA required · full catalog CRUD
Admin actionSPA call Data touchedPolicy / trigger Response
Create cohortINSERT cohorts · cohort_memberscohorts · cohort_membersis_staff() · aal2 requiredNew cohort visible in Trainee journeys
Create userGoTrue admin API + INSERT profilesauth.users · profiles · email_outboxstaff-only INSERT · trigger enqueues welcome emailUser receives welcome email · redirected to /change-password
Assign roleUPDATE profiles.role_idprofilesprofiles_guard_priv_cols allows staffRole change effective on next JWT refresh
Author contentINSERT contents · upload to bucketcontents · content-filesstaff-only write on contentsTrainees in cohort see the item
Publish assessmentINSERT assessments · assessment_questionsassessments · assessment_questions (base)staff-only writeTrainees see quiz via _public view
Assign mentor to traineeINSERT mentor_traineesmentor_traineesstaff-onlyMentor sees trainee in their inbox
Assign partnerINSERT partner_assignmentspartner_assignments (agency + track)staff-onlyPartner sees trainee in their roster
Send nudge / surveyINSERT nudges / surveysnudges · surveys · survey_questionsstaff-onlyAppears on trainee dashboards
Handle escalationUPDATE capstone_escalationscapstone_escalations · notificationsstaff-onlyFacilitator + team notified

Partner portal · /partner/*

10 external agencies · role partner · MFA required · strict agency + track scope
Partner actionSPA call Data touchedPolicy / trigger Response
View my traineesGET /rest/v1/profiles · partner_assignmentspartner_assignments · profilesscope: partner_id = auth.uid()Only assigned trainees returned
Open trainee detailGET progress · IDP · JDmodule_progress · content_progress · assessment_attemptspartner scope through assignmentRead-only view rendered
Create OJT rotation planINSERT rotation_plans + helperrotation_plans · rotation_months · rotation_month_stagespartner scope through the assigned traineeTrainee sees the plan in /user/rotation
Write JD noteUPDATE job_descriptionsjob_descriptionspartner can write on own trainees onlyNote appears on trainee JD panel
Update own profileUPDATE profiles (self)profiles (non-privileged cols)profiles_guard_priv_cols blocks role changeProfile updated

Mentor portal · /mentor/*

15 SAMA senior staff · role mentor · MFA required
Mentor actionSPA call Data touchedPolicy / trigger Response
View inboxGET mentor_bookingsmentor_bookingsscope: mentor_id = auth.uid()List of upcoming 1:1s
Set availabilityUPSERT mentor_availability_rulesmentor_availability_rules · mentor_slotsowner scope · RPC generate_mentor_slots_from_ruleSlots regenerated automatically
Cancel bookingUPDATE mentor_bookings.statusmentor_bookings · notificationsown booking onlyTrainee notified
Write trainee reviewINSERT trainee_task_reviewstrainee_task_reviewsmentor scope through assignmentTrainee sees feedback
Update profile photoPOST mentor-photosstorage.objectsowner-scoped INSERTPhoto visible on mentor cards

Line Manager portal · /line-manager/*

20 department managers · role line_manager · MFA required
LM actionSPA call Data touchedPolicy / trigger Response
View direct reportsGET line_manager_trainees + profilesline_manager_trainees · profilesscope: line_manager_id = auth.uid()List of reports with status
Approve leaveUPDATE annual_leave_requestsannual_leave_requestsleave_guard_status + is_lm_or_staff()Trainee notified
Grade OJT result and add feedbackUPDATE rotation_month_stagesrotation_month_stagesstage_guard_completion + is_lm_or_staff()Corrected result and feedback become visible to the trainee
Read assignment submissionsGET assignment_submissionsassignment_submissionsLM scope on own traineesRead-only view

Facilitator portal · /facilitator/*

SAMA staff · role facilitator · MFA required · capstone review queue
Facilitator actionSPA call Data touchedPolicy / trigger Response
Read submission queueGET capstone_submissionscapstone_submissionsis_facilitator_or_staff()All submissions across teams
Approve submissionINSERT capstone_submission_reviewscapstone_submission_reviews · notificationsfacilitator scopeTeam notified · status=approved
Send back for revisionsame as above with status=revisecapstone_submission_reviewsfacilitator scopeTeam sees revision request
EscalateINSERT capstone_escalationscapstone_escalationsfacilitator scopeAdmin sees on /admin/escalations

Reports portal · /reports/*

Read-only analytics · roles admin / td_team · MFA required · zero writes
Reader actionSPA call Data touchedPolicy / trigger Response
Overview KPIsGET aggregated countsMultiple tables · views · RPCsstaff readKPI tiles rendered
Cohort scorecardsGET cohorts + progresscohorts · module_progressstaff readGrid of scorecards
Cohort detailGET attendance · assessmentssession_attendance · assessment_attemptsstaff readDeep-dive charts
Content engagementGET content_progress rollupcontent_progress · contentsstaff readViews · completions · watch time

Role-to-role interaction matrix

Where any two roles collaborate on the platform. Use this row-by-row to build a sequence diagram between the pair.
PairDirection of the flowArtifact exchangedMediated by
Trainee ↔ MentorBooking · session · reviewmentor_bookings · trainee_task_reviewsPostgREST · notifications
Trainee ↔ Line ManagerLeave request · OJT result grading + feedbackannual_leave_requests · rotation_month_stagesPostgREST + LM/staff RLS · Storage
Trainee ↔ PartnerOJT plan creation · JD notespartner_assignments · rotation_plans · rotation_months · rotation_month_stages · job_descriptionsPostgREST scoped by partner_id
Trainee (team) ↔ FacilitatorCapstone submission → reviewcapstone_submissions · capstone_submission_reviews · escalationsPostgREST + facilitator RLS · Storage · notifications
Trainee ↔ AdminContent · nudges · surveys · emailcontents · nudges · surveys · email_outboxPostgREST · Edge Function drain · Resend (via DE-NET)
Mentor ↔ AdminAssignment · auditmentor_traineesPostgREST staff-write
Partner ↔ AdminAssignment · agency + trackpartner_assignmentsPostgREST staff-write
LM ↔ AdminAssignment · leave windowsline_manager_trainees · annual_leave_windowsPostgREST staff-write
Facilitator ↔ AdminEscalationcapstone_escalationsPostgREST · notifications
Everyone ↔ ReportsRead-only feedsAggregated views + RPCsPostgREST SELECT only

Data model

Every business table lives in public. Every table has RLS enabled. Column-guarding triggers layer on top for the columns that require role-based writes.

Catalog · authored by staff

TableReadWrite
tracksauthenticatedstaff
cohorts, cohort_membersauthenticatedstaff
modules, topicsauthenticatedstaff
contents, content_slidesauthenticatedstaff
assessmentsauthenticatedstaff
assessment_questionsstaff (base) · _public view (trainees)staff
assignmentsauthenticatedstaff
surveys, survey_questionsauthenticatedstaff
nudges, email_templatesauthenticatedstaff
roles, permissions, role_permissionsauthenticatedstaff

Workflow · triggers guard writes

TableOwnershipGuarded columns
assessment_attemptstrainee · ownscore · is_passed · earned_points → staff only
rotation_planspartner creates for assigned traineesstatus → partner/staff within assignment scope
rotation_month_stagestrainee uploads evidence · Line Manager gradesresult · feedback · is_completed · completed_at → LM/staff only
annual_leave_requestsowner submitsstatus → LM/staff only
capstone_submissionsteam leaderreviewed by facilitator
capstone_submission_reviewsfacilitator/stafffull row
capstone_escalationsfacilitatoradmin acts on them
mentor_bookingstrainee creates · mentor cancels
team_chat_messagesmember of the rosterINSERT check: user_id = auth.uid()
notificationsowner reads / marks readINSERT: staff only
email_outboxstaff alldrain runs as service role
profilesowner reads all · updates non-privileged colsrole · role_id · email · id → staff only

Row Level Security helpers

Every policy that gates by role uses one of these SQL helpers. All are SECURITY DEFINER with a pinned search_path.
HelperRole setMFA requirement
public.is_staff()admin, super_adminaal2 required if the caller has a verified factor
public.is_admin_or_super()admin, super_adminsame
public.is_facilitator_or_staff()admin, super_admin, facilitatorsame
public.is_lm_or_staff()admin, super_admin, line_manager, td_teamsame
public.is_mentor_or_staff()admin, super_admin, mentor, facilitator, td_teamsame

Grandfathering: if the caller has NO verified factor yet, aal2 is not required, so a newly-provisioned admin can log in at aal1 and complete MFA enrollment. Once they enroll and challenge, every subsequent request lands at aal2 and the helpers keep returning true.