Skoolsewa - Ecommerce Docs
Developer ResourcesProfile

My Profile

The self-profile surface — what a person sees about their own record, which blocks appear for which person type, and the two categories of data deliberately excluded.

My Profile

GET /api/profile and /dashboard/profile. One screen that shows a person their own record, assembled from whichever person tables hold a row for them.

1. Documentation Evidence

Source TypeFilesWhat Was Extracted
Endpointapps/api/src/modules/profile/profile.controller.tsGuards, decorator, allowlist rationale
Query surfaceapps/api/src/modules/profile/profile.service.tsThe four self-scoped reads and the two nested-list joins
Response shapeapps/api/src/modules/profile/dto/profile.dto.tsBlocks, nullability, what is absent
Behaviourapps/api/src/modules/profile/profile.service.int.spec.ts13 cases against a real database
Panelapp/(dashboard)/dashboard/profile/page.tsxSection composition and the grid

2. Summary

FieldValue
Who can call itAny authenticated session with an active role
What it returnsThe caller's own record. Never another person's
PermissionNone declared. Allowlisted in NO_PERMISSION_ADMIN_HANDLERS
Why that is safeOwnership, not permission — see below
WritesNone. This is a read surface

3. Which blocks appear

person is always present. student, staff and guardian are each null unless the caller holds a live row in that table.

Person typeBlocks
Pupilperson + student (with their guardians)
Teacher or other staffperson + staff
Parent or guardianperson + guardian (with their children)
System or custom-role operatorperson only
A teacher whose child attendsperson + staff + guardianboth

The discriminator is the record, never the role name

A person's type is decided by which person-record rows exist for their user_id, not by activeRole.name. role.name is a mutable text column; deriving behaviour from it is a defect this codebase has already removed once, from PeopleAccessService.

It also gets the overlap right. The unique constraints (students_user_id_unique and its siblings) make "at most one of each" true, but nothing makes them mutually exclusive — so the blocks are independent rather than a switch.

4. What this endpoint does NOT return, and why

Two categories are absent by design:

AbsentGated byLives at
basicSalary, allowances, totalSalary, bank, PAN, citizenship, SSF, CITStaffSalary_READGET /staff/:id/salary
medicalConditions, allergies, specialNeedsStudentMedical_READGET /students/:id/medical

These are the only two modules in the system that gate at the field level rather than the row level, and each already has a dedicated service that re-checks the permission. student-medical.service.ts states the failure this avoids: "the moment any aggregate response composes this method to save a round trip, it is off the code path and nothing is left." This endpoint is that aggregate, and it does not compose them.

An earlier design returned both, self-only, on the argument that requiring the permission would mean only people who can see everyone's pay could see their own. That argument is false: row scope comes from activeRole.scopeKind, not from holding the permission (people-access.service.ts). A self-scoped role holding StaffSalary_READ already reads its own pay and nobody else's.

Adding own-pay here later is therefore not a one-line change. The staff and teacher roles are seeded scope_kind = 'all', so granting them StaffSalary_READ would return { kind: "all" } and expose every colleague's salary. It needs a self-scoped role or a self-only read that does not route through scopeFor.

5. Cross-person data

Two lists cross from the caller's record to another person's: a student's guardians and a guardian's children. Both carry name, relationship and primary flag only — no phone number, no admission number. Neither is needed to answer "who are my children", and this route declares no permission, so it says the least that answers the question.

Both filter liveness twice — on the person-type row and on that person's users row. student_guardian has no deleted_at of its own and a link outlives a soft-deleted person, so filtering the link alone would list a removed guardian as current. In a school that is a safeguarding record, not a tidiness detail.

6. Not applicable

No schema change, no migration, no queue job, no realtime event, no cache. The feature reads existing columns.