Linking a parent to a pupil
How a school gives a parent or guardian access to one pupil in the parent portal, and how EduSpaze makes sure the right person is linked to the right child. Decided 2026-10-21.
1. Where it happens
Students → a pupil → Parents and guardians (/school/students/<id>). Module school.guardians (sensitive): Create invites, Edit resends and restricts, Delete cancels and removes. Only the shipped School admin holds it; a school can give it to its safeguarding lead in its own roles. The section is shown only while parent access is on for the platform (Admin → Settings → Modules); with the school's own switch off, links can be prepared but the parent sees nothing until it is on.
2. The process
- The school invites. Name, email, relationship, and two ticks the admin makes deliberately: *they have parental responsibility, as our records show*, and *nothing stops them seeing this child's information — no court order, no safeguarding concern*. Without both, no invitation.
- A single-use link is emailed to the address entered (
GUARDIAN_INVITE). The email never names the child. There is no separate code: the school asked for the email alone to be enough (2026-10-22), so the link is what proves the person is the one the school invited. - The parent opens the link. Opening it spends nothing (mail scanners open every link). The page names the school and the child's first name, and the parent confirms *"Yes, {child} is my child."* Then they choose a password — or, if they already have a parent account (a brother or sister at this or another school), enter its password.
- Then, and only then, the GUARDIAN account (if new) and the
GuardianRelationshipare created: sourceMANUAL, parental responsibility as asserted,contactSuppressed = false. They are signed in to/parent, where the four gates (lib/parent/gates.ts) decide what they see.
The email address is therefore the control: check it before sending — a mistyped address sends the link to whoever owns it. If that happens, Cancel the invitation (or Remove the link if it was already accepted) at once. A guessed link is useless: it is 32 random bytes, stored only as a hash. The link lasts 14 days. Resend issues a new link; the old one stops working. If the email fails to send, the admin is shown the link to pass on themselves.
3. Afterwards
- Restrict a link and the parent sees nothing of that child, at once, and is not told why (gate G2). Lift it only when the school is sure it may.
- Remove a link: that child is gone from the parent's account. The account stays — they may have other children.
- Links from the school's SIS or the demo seed show their source; the same controls apply.
4. Records
Every step is on the school's audit log: guardian.invited, guardian.invite_resent, guardian.linked, guardian.restricted, guardian.unrestricted, guardian.invite_revoked, guardian.unlinked. The invitation stores a hash of the link, never the link itself.
5. What changed
Until 2026-10-21 guardian accounts came only from the seed, standing in for an invitation flow that did not exist (discovery.md row 5), and a test held that no runtime path could grant the guardian role. That test now holds that exactly one path can — acceptInvite in lib/school/guardians.ts, after the emailed link is checked — and that the only other mention is signing in the account it has just made. There is still no self-registration: a parent cannot ask for access, only accept a school's invitation.
On 2026-10-22 the separate school code was dropped at the school's request: handing a code over in person was a second step schools did not want. GuardianInvite.codeHash is now nullable and unused (migration 20261022150000_guardian_invite_no_code); invitations made before that still work by their link alone.
