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.
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)
Tier 2 · Application (Supabase services)
| Service | Prefix | Responsibility | Security |
|---|---|---|---|
| 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.*)
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 hashauth.identities · sessions · refresh_tokensauth.mfa_factors · mfa_challenges · mfa_amr_claims
Object storage · 6 buckets
| Bucket | Purpose | Policy |
|---|---|---|
content-files | Course PDF / video / PPT / DOCX | Owner-scoped write · staff bypass · MIME allow list (no SVG) |
cohort-files | Cohort docs | Owner-scoped write · staff bypass |
mentor-photos | Mentor profile pictures | Owner-scoped · authenticated read |
capstone-submissions | Team deliverables | Team-scoped read · facilitator/staff bypass |
rotation-evidence | OJT stage evidence | Owner-scoped · assigned Line Manager can review |
team-chat | Chat attachments | Team-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/*
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/*
admin / super_admin · MFA required · full catalog CRUD| Admin action | SPA call | Data touched | Policy / trigger | Response |
|---|---|---|---|---|
| Create cohort | INSERT cohorts · cohort_members | cohorts · cohort_members | is_staff() · aal2 required | New cohort visible in Trainee journeys |
| Create user | GoTrue admin API + INSERT profiles | auth.users · profiles · email_outbox | staff-only INSERT · trigger enqueues welcome email | User receives welcome email · redirected to /change-password |
| Assign role | UPDATE profiles.role_id | profiles | profiles_guard_priv_cols allows staff | Role change effective on next JWT refresh |
| Author content | INSERT contents · upload to bucket | contents · content-files | staff-only write on contents | Trainees in cohort see the item |
| Publish assessment | INSERT assessments · assessment_questions | assessments · assessment_questions (base) | staff-only write | Trainees see quiz via _public view |
| Assign mentor to trainee | INSERT mentor_trainees | mentor_trainees | staff-only | Mentor sees trainee in their inbox |
| Assign partner | INSERT partner_assignments | partner_assignments (agency + track) | staff-only | Partner sees trainee in their roster |
| Send nudge / survey | INSERT nudges / surveys | nudges · surveys · survey_questions | staff-only | Appears on trainee dashboards |
| Handle escalation | UPDATE capstone_escalations | capstone_escalations · notifications | staff-only | Facilitator + team notified |
Partner portal · /partner/*
partner · MFA required · strict agency + track scope| Partner action | SPA call | Data touched | Policy / trigger | Response |
|---|---|---|---|---|
| View my trainees | GET /rest/v1/profiles · partner_assignments | partner_assignments · profiles | scope: partner_id = auth.uid() | Only assigned trainees returned |
| Open trainee detail | GET progress · IDP · JD | module_progress · content_progress · assessment_attempts | partner scope through assignment | Read-only view rendered |
| Create OJT rotation plan | INSERT rotation_plans + helper | rotation_plans · rotation_months · rotation_month_stages | partner scope through the assigned trainee | Trainee sees the plan in /user/rotation |
| Write JD note | UPDATE job_descriptions | job_descriptions | partner can write on own trainees only | Note appears on trainee JD panel |
| Update own profile | UPDATE profiles (self) | profiles (non-privileged cols) | profiles_guard_priv_cols blocks role change | Profile updated |
Mentor portal · /mentor/*
mentor · MFA required| Mentor action | SPA call | Data touched | Policy / trigger | Response |
|---|---|---|---|---|
| View inbox | GET mentor_bookings | mentor_bookings | scope: mentor_id = auth.uid() | List of upcoming 1:1s |
| Set availability | UPSERT mentor_availability_rules | mentor_availability_rules · mentor_slots | owner scope · RPC generate_mentor_slots_from_rule | Slots regenerated automatically |
| Cancel booking | UPDATE mentor_bookings.status | mentor_bookings · notifications | own booking only | Trainee notified |
| Write trainee review | INSERT trainee_task_reviews | trainee_task_reviews | mentor scope through assignment | Trainee sees feedback |
| Update profile photo | POST mentor-photos | storage.objects | owner-scoped INSERT | Photo visible on mentor cards |
Line Manager portal · /line-manager/*
line_manager · MFA required| LM action | SPA call | Data touched | Policy / trigger | Response |
|---|---|---|---|---|
| View direct reports | GET line_manager_trainees + profiles | line_manager_trainees · profiles | scope: line_manager_id = auth.uid() | List of reports with status |
| Approve leave | UPDATE annual_leave_requests | annual_leave_requests | leave_guard_status + is_lm_or_staff() | Trainee notified |
| Grade OJT result and add feedback | UPDATE rotation_month_stages | rotation_month_stages | stage_guard_completion + is_lm_or_staff() | Corrected result and feedback become visible to the trainee |
| Read assignment submissions | GET assignment_submissions | assignment_submissions | LM scope on own trainees | Read-only view |
Facilitator portal · /facilitator/*
facilitator · MFA required · capstone review queue| Facilitator action | SPA call | Data touched | Policy / trigger | Response |
|---|---|---|---|---|
| Read submission queue | GET capstone_submissions | capstone_submissions | is_facilitator_or_staff() | All submissions across teams |
| Approve submission | INSERT capstone_submission_reviews | capstone_submission_reviews · notifications | facilitator scope | Team notified · status=approved |
| Send back for revision | same as above with status=revise | capstone_submission_reviews | facilitator scope | Team sees revision request |
| Escalate | INSERT capstone_escalations | capstone_escalations | facilitator scope | Admin sees on /admin/escalations |
Reports portal · /reports/*
admin / td_team · MFA required · zero writes| Reader action | SPA call | Data touched | Policy / trigger | Response |
|---|---|---|---|---|
| Overview KPIs | GET aggregated counts | Multiple tables · views · RPCs | staff read | KPI tiles rendered |
| Cohort scorecards | GET cohorts + progress | cohorts · module_progress | staff read | Grid of scorecards |
| Cohort detail | GET attendance · assessments | session_attendance · assessment_attempts | staff read | Deep-dive charts |
| Content engagement | GET content_progress rollup | content_progress · contents | staff read | Views · completions · watch time |
Role-to-role interaction matrix
| Pair | Direction of the flow | Artifact exchanged | Mediated by |
|---|---|---|---|
| Trainee ↔ Mentor | Booking · session · review | mentor_bookings · trainee_task_reviews | PostgREST · notifications |
| Trainee ↔ Line Manager | Leave request · OJT result grading + feedback | annual_leave_requests · rotation_month_stages | PostgREST + LM/staff RLS · Storage |
| Trainee ↔ Partner | OJT plan creation · JD notes | partner_assignments · rotation_plans · rotation_months · rotation_month_stages · job_descriptions | PostgREST scoped by partner_id |
| Trainee (team) ↔ Facilitator | Capstone submission → review | capstone_submissions · capstone_submission_reviews · escalations | PostgREST + facilitator RLS · Storage · notifications |
| Trainee ↔ Admin | Content · nudges · surveys · email | contents · nudges · surveys · email_outbox | PostgREST · Edge Function drain · Resend (via DE-NET) |
| Mentor ↔ Admin | Assignment · audit | mentor_trainees | PostgREST staff-write |
| Partner ↔ Admin | Assignment · agency + track | partner_assignments | PostgREST staff-write |
| LM ↔ Admin | Assignment · leave windows | line_manager_trainees · annual_leave_windows | PostgREST staff-write |
| Facilitator ↔ Admin | Escalation | capstone_escalations | PostgREST · notifications |
| Everyone ↔ Reports | Read-only feeds | Aggregated views + RPCs | PostgREST 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
| Table | Read | Write |
|---|---|---|
tracks | authenticated | staff |
cohorts, cohort_members | authenticated | staff |
modules, topics | authenticated | staff |
contents, content_slides | authenticated | staff |
assessments | authenticated | staff |
assessment_questions | staff (base) · _public view (trainees) | staff |
assignments | authenticated | staff |
surveys, survey_questions | authenticated | staff |
nudges, email_templates | authenticated | staff |
roles, permissions, role_permissions | authenticated | staff |
Workflow · triggers guard writes
| Table | Ownership | Guarded columns |
|---|---|---|
assessment_attempts | trainee · own | score · is_passed · earned_points → staff only |
rotation_plans | partner creates for assigned trainees | status → partner/staff within assignment scope |
rotation_month_stages | trainee uploads evidence · Line Manager grades | result · feedback · is_completed · completed_at → LM/staff only |
annual_leave_requests | owner submits | status → LM/staff only |
capstone_submissions | team leader | reviewed by facilitator |
capstone_submission_reviews | facilitator/staff | full row |
capstone_escalations | facilitator | admin acts on them |
mentor_bookings | trainee creates · mentor cancels | — |
team_chat_messages | member of the roster | INSERT check: user_id = auth.uid() |
notifications | owner reads / marks read | INSERT: staff only |
email_outbox | staff all | drain runs as service role |
profiles | owner reads all · updates non-privileged cols | role · role_id · email · id → staff only |
Row Level Security helpers
SECURITY DEFINER with a pinned search_path.| Helper | Role set | MFA requirement |
|---|---|---|
public.is_staff() | admin, super_admin | aal2 required if the caller has a verified factor |
public.is_admin_or_super() | admin, super_admin | same |
public.is_facilitator_or_staff() | admin, super_admin, facilitator | same |
public.is_lm_or_staff() | admin, super_admin, line_manager, td_team | same |
public.is_mentor_or_staff() | admin, super_admin, mentor, facilitator, td_team | same |
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.
Security posture
Where the platform stands after the September 2026 security assessment and the closing migrations (087 → 121). Most findings closed; a small hardening backlog remains for ops config.
Closed · shipped
Open · scheduled
Password policy currently enforced
Operations & disaster recovery
Backup cadence, recovery targets, deploy path, and the runbooks that keep the platform on the air.
Backup & retention
pg_basebackupRecovery targets
Deploy path
- Developer opens PR on
main. - CI runs typecheck (
tsc --noEmit), unit tests (Vitest), and deletion-completeness tests. - Merged commit builds a Vite bundle to
react-app/dist. - Bundle uploaded to CDN.
index.htmlis atomic-replaced last so no half-shipped state is visible. - Supabase migrations under
react-app/supabase/applied numerically. Each migration is idempotent. - Post-deploy: smoke test each portal login (Trainee, Admin, Partner, Mentor, LM, Facilitator, Reports).
Runbooks (documented)
- Password recovery for a specific account: SQL
UPDATE auth.users SET encrypted_password = crypt(...) - Rotate the shared password (mass):
113_rotate_shared_password.sql→ export → HR handover →114_wipe_password_bootstrap.sql - Disable MFA temporarily (DAST scan, demo):
107_mfa_off_temporary.sql→ run scan →108_mfa_on_restore.sql - Import a new cohort: extend
120_import_spp_participants.sqlgenerator to point at the new XLSX - Cleanup after a DAST scan: purge test accounts from
auth.usersand re-run the security migrations