Problem Statement
Campus access is run entirely on a paper register at the gate, and it has a specific, serious failure: the school cannot answer "who is on campus right now".
That question is only ever asked during a fire drill or a real emergency, which is exactly when a paper register two buildings away is useless. In practice the register also fails in ordinary ways:
- Visitors sign in and leave without signing out, so the register shows people on site who went home hours ago. After a week the "currently inside" count is fiction.
- There is no host approval. Anyone can write a teacher's name in the Person to meet column and walk in; the teacher finds out when the visitor arrives at their classroom.
- Students are released to whoever turns up. A parent collecting a child mid-day signs the same register. There is no check that the person collecting is authorised to collect that child. This is the one that should not wait.
- Nothing is searchable. "Has this person been here before" needs somebody to leaf through a book.
Proposed Solution
A gate module built on one model covering both directions of movement, because they are the same object — a person who is inside and needs to be accounted for.
backend/models/VisitorPass.js
passType — visitor (an outsider coming in) or gate-pass (a student going out)
- Visitor identity: name, phone, email, organisation, ID proof type and a masked ID number, vehicle number, number of accompanying people
host / hostName — the staff member being visited, for visitor passes
student / studentName / className and guardianName / guardianRelation — for gate-pass
purpose, expectedDuration, status, approvalStatus, checkInAt, checkOutAt, badgeNumber, securityNotes
movements[] — an append-only audit trail of every state change with actor and timestamp
The four things this has to get right
1. "Currently on campus" must be a query, not a guess. A partial index on status: 'checked-in' makes the live roll a single indexed lookup, so the evacuation list is instant and always current.
2. The same person cannot be checked in twice. A unique partial index over (idNumber, status: 'checked-in') makes a duplicate open pass impossible at the database level rather than by convention. Doing this in application code means it holds only as long as every future code path remembers to check.
3. Check-out must be atomic and cannot precede check-in. Check-out is a conditional update filtered on status: 'checked-in'; a double-tap on the gate tablet matches nothing the second time and returns 409 rather than overwriting a check-out time and corrupting the duration. A pass that was never checked in cannot be checked out at all.
4. A student is released only to an authorised guardian. A gate-pass requires an explicit staff approval before check-out is permitted, recording who approved it. No approval, no release — enforced in the handler, not in the UI.
Overstay reconciliation
Every pass carries expectedDuration. A pass still open past its expected departure is flagged overstayed in query results, and the module exposes a bulk reconciliation endpoint that force-closes stale passes at end of day with an explicit auto-closed marker — so the register is honest about the difference between "signed out" and "we assumed they left". That distinction is the thing the paper register loses.
API — /api/visitors
| Method |
Path |
Who |
| POST |
/passes |
admin / staff — register a visitor or gate pass |
| GET |
/passes |
admin / staff — filters + overstayed |
| GET |
/passes/on-campus |
admin / staff — the live roll |
| GET |
/passes/:id |
admin / staff / the host |
| PATCH |
/passes/:id/approve |
the named host, or admin |
| PATCH |
/passes/:id/check-in |
admin / staff |
| PATCH |
/passes/:id/check-out |
admin / staff — atomic |
| PATCH |
/passes/:id/cancel |
admin / staff |
| POST |
/passes/reconcile |
admin — bulk close stale passes |
| GET |
/my-approvals |
teacher — visitors waiting on me |
| GET |
/stats |
admin / staff |
Privacy
ID numbers are stored masked (last four characters only). A gate register does not need to retain a full government ID number, and one that does is a liability the school does not need to carry.
Frontend
A /visitors page. Security and office staff get the gate desk — register, approve, check in/out, and the live on-campus roll with overstays highlighted. Teachers see only the visitors waiting on their approval.
Alternative Approaches
Separate models for visitors and gate passes. They share the check-in/check-out/audit machinery entirely; splitting them means writing the atomic check-out twice and having two answers to "who is on campus".
Skip host approval. That is the current situation.
Store full ID numbers for a stronger audit trail. The marginal audit value does not justify holding a database of government ID numbers behind a school gate terminal.
Affected Area
Mockups / Additional Context
The database-level uniqueness on open passes matters more than it looks: it is the difference between an invariant and a habit.
I'd like to pick this one up.
Problem Statement
Campus access is run entirely on a paper register at the gate, and it has a specific, serious failure: the school cannot answer "who is on campus right now".
That question is only ever asked during a fire drill or a real emergency, which is exactly when a paper register two buildings away is useless. In practice the register also fails in ordinary ways:
Proposed Solution
A gate module built on one model covering both directions of movement, because they are the same object — a person who is inside and needs to be accounted for.
backend/models/VisitorPass.jspassType—visitor(an outsider coming in) orgate-pass(a student going out)host/hostName— the staff member being visited, forvisitorpassesstudent/studentName/classNameandguardianName/guardianRelation— forgate-passpurpose,expectedDuration,status,approvalStatus,checkInAt,checkOutAt,badgeNumber,securityNotesmovements[]— an append-only audit trail of every state change with actor and timestampThe four things this has to get right
1. "Currently on campus" must be a query, not a guess. A partial index on
status: 'checked-in'makes the live roll a single indexed lookup, so the evacuation list is instant and always current.2. The same person cannot be checked in twice. A unique partial index over
(idNumber, status: 'checked-in')makes a duplicate open pass impossible at the database level rather than by convention. Doing this in application code means it holds only as long as every future code path remembers to check.3. Check-out must be atomic and cannot precede check-in. Check-out is a conditional update filtered on
status: 'checked-in'; a double-tap on the gate tablet matches nothing the second time and returns 409 rather than overwriting a check-out time and corrupting the duration. A pass that was never checked in cannot be checked out at all.4. A student is released only to an authorised guardian. A
gate-passrequires an explicit staff approval before check-out is permitted, recording who approved it. No approval, no release — enforced in the handler, not in the UI.Overstay reconciliation
Every pass carries
expectedDuration. A pass still open past its expected departure is flaggedoverstayedin query results, and the module exposes a bulk reconciliation endpoint that force-closes stale passes at end of day with an explicitauto-closedmarker — so the register is honest about the difference between "signed out" and "we assumed they left". That distinction is the thing the paper register loses.API —
/api/visitors/passes/passesoverstayed/passes/on-campus/passes/:id/passes/:id/approve/passes/:id/check-in/passes/:id/check-out/passes/:id/cancel/passes/reconcile/my-approvals/statsPrivacy
ID numbers are stored masked (last four characters only). A gate register does not need to retain a full government ID number, and one that does is a liability the school does not need to carry.
Frontend
A
/visitorspage. Security and office staff get the gate desk — register, approve, check in/out, and the live on-campus roll with overstays highlighted. Teachers see only the visitors waiting on their approval.Alternative Approaches
Separate models for visitors and gate passes. They share the check-in/check-out/audit machinery entirely; splitting them means writing the atomic check-out twice and having two answers to "who is on campus".
Skip host approval. That is the current situation.
Store full ID numbers for a stronger audit trail. The marginal audit value does not justify holding a database of government ID numbers behind a school gate terminal.
Affected Area
Mockups / Additional Context
The database-level uniqueness on open passes matters more than it looks: it is the difference between an invariant and a habit.
I'd like to pick this one up.