1. What it does
Attendance is taken by recognising a face, not by passing a sheet of paper round a hall.
A student creates an account and immediately registers their face. From then on the portal will not open until the camera has confirmed they are who the account says they are. In class, a lecturer points a camera at the door and students are marked present or late as they walk past.
Three kinds of people use it, and the system treats them differently:
- Students sign themselves up, register a face once, and see their own attendance percentage per course.
- Lecturers sign in on the same page, register a face like anybody else, then run class sessions and correct anything the camera got wrong.
- Administrators sign in at a separate address, and see every member, every face event and every setting.
2. Architecture
The shape is deliberately old fashioned: a server that renders three HTML
shells, and one JSON endpoint that everything else talks to. There is no build
step, no bundler, no node_modules. You upload the files and it
runs.
What happens when a student signs in
Note what does not travel: no photograph is uploaded during a check. The browser sends 128 floating point numbers. The picture stored against an account is a small thumbnail, kept only so a human administrator can recognise the row in a list.
The three shells
/admin on any web server.Why one API file
A single api.php with a switch on the action means
there is exactly one place where authentication, CSRF checking and error
handling happen. With twenty separate endpoint files it is far too easy for one
of them to forget a guard. Every request passes through the same three lines
before it reaches any handler.
3. Front end
#/sessions or #/roll?id=4. Back and forward work as expected.Why no framework
React would have meant a build step, a bundler and a dist folder,
which turns "upload the files" into a deployment process. It would also have
added roughly forty times more JavaScript than this project actually needs. The
whole front end here is about 3,034 lines across five files, and a marker can read all of it.
Responsive strategy
Not a mobile site and a desktop site, one layout that reorganises at 900 pixels. On a desktop the navigation is a fixed sidebar listing every page. On a phone that sidebar is replaced by a floating bar at the bottom with the four most used destinations and a sheet that slides up for the rest, because the bottom of a phone screen is where a thumb actually reaches.
Modals rise from the bottom edge on a phone and centre on a desktop. Tables scroll sideways inside their own container so the page itself never does. Safe area insets are respected, so nothing sits under an iPhone home indicator.
File inventory
Server side
| File | What it does | Lines | Size |
|---|---|---|---|
| index.php | Sign in, sign up, face registration and face verification. Four panels, one visible at a time. | 270 | 11 KB |
| portal.php | Student portal shell. Refuses to render without a session and a passed face check. | 124 | 5 KB |
| admin/index.php | Staff console shell, and its own sign in for administrators. | 209 | 9 KB |
| api.php | Every server operation. One endpoint, routed on an action parameter. | 1,269 | 44 KB |
| includes/config.php | Database credentials, timezone, session hardening, PDO connection. | 122 | 5 KB |
| includes/functions.php | Auth guards, CSRF, logging, and the face matching maths. | 420 | 11 KB |
| includes/bootguard.php | Inline failsafe so a broken asset can never leave a blank page. | 154 | 6 KB |
| check.php | Installation diagnostic. Reports what is wrong and the command to fix it. | 386 | 17 KB |
| face-test.php | Standalone recognition test. No database, no account. | 175 | 7 KB |
Browser side
| File | What it does | Lines | Size |
|---|---|---|---|
| assets/css/app.css | The entire visual system. Design tokens, layout, components, print rules. | 492 | 23 KB |
| assets/js/core.js | API client, icon set, toasts, modal, shared render helpers. | 250 | 10 KB |
| assets/js/face.js | Face engine, the measurements behind the coaching, the capture stage. | 739 | 24 KB |
| assets/js/auth.js | The gate. Sign in, sign up, registration and verification flow. | 387 | 12 KB |
| assets/js/portal.js | Student screens and the hash router that swaps between them. | 406 | 19 KB |
| assets/js/admin.js | Console screens, including the live recognition desk. | 1,252 | 61 KB |
| assets/js/face-api.js | The recognition library itself, served from this server. | 5,009 | 1.3 MB |
4. Back end
Request handling
Every request to api.php passes the same gauntlet before a handler
is called:
- The CSV export leaves early, because it writes its own headers.
verify_csrf()rejects any POST without a matching token.- The action is matched against the router. Anything unknown is a 404.
- The handler calls a guard:
require_login,require_verified,require_stafforrequire_admin. - Database exceptions are caught centrally. A duplicate key becomes a plain sentence rather than a stack trace.
The guards, and why there are four
The distinction between the first two is the whole point of the project. A
signed in student who has not passed the face check can call exactly two things:
register a face, or verify one. Everything else answers
403.
Responses
{ "ok": true, "data": { ... } }
{ "ok": false, "error": "A sentence a student can act on." }
Error messages are written for the person reading them, not for a developer. "That email address and password do not match an account" rather than "Authentication failed". The one place this is deliberately vague is sign in, where saying which half was wrong would tell an attacker which emails exist.
5. Face recognition
Three neural networks run in the browser. The server never sees a face, only the numbers that describe one.
The pipeline
- Detection. A Tiny Face Detector, a compact convolutional network, scans the frame and returns a box around each face with a confidence score. It runs at 320 pixels input, which is the trade-off that keeps a mid range phone smooth.
- Landmarks. A second network places 68 points on the face: jaw line, eyebrows, eyes, nose, mouth. The face is then rotated and scaled so that every face reaches the next stage the same way up and the same size.
- Embedding. A ResNet style recognition network turns the aligned face into 128 floating point numbers. It was trained so that two pictures of one person land close together in that 128 dimensional space, and two different people land far apart.
models/ on this server.Comparing two faces
Similarity is straight line distance in 128 dimensions:
d(A,B) = sqrt( sum over i of (Ai - Bi)^2 ), i = 1 .. 128
Small distance means the same person. The percentage shown on screen is simply
1 - d, which is easier to read than a raw distance but carries the
same information.
Two different comparisons, on purpose
This distinction matters more than any threshold tuning. Searching a hundred faces gives a hundred chances for a coincidental match. Searching one gives one. Sign in is therefore the accurate case, and it is also the case that protects the account, which is why its threshold is the stricter of the two.
The browser proposes, the server decides
Matching runs twice. The browser does it first so the camera stays responsive,
then sends the raw 128 numbers to PHP, where
verify_against_user() or identify_face() runs the same
comparison again before a single row is written.
This is not redundancy for its own sake. If the JavaScript were the only judge, a student could edit it in the browser console and claim to be anybody. Because the server re-runs the comparison against the stored template, a forged claim simply fails.
Anti spoofing
- Blink check. The eye aspect ratio, the gap between eyelids relative to eye width, drops sharply for the moment an eye closes. A printed photograph cannot blink. Currently on, switchable in the console.
- One face only. Capture stops if more than one face is in frame.
- Duplicate identity. A face already belonging to another account is refused at registration.
- Lockout. 5 failed checks ends the session. Every attempt is logged with its distance, address and device.
- Cooldown. The same face cannot be scanned into a session twice within 8 seconds, and a unique key on the attendance table makes double marking impossible regardless.
6. Capture coaching
Bad recognition is almost always bad input. Most of the work in the capture screen is spent stopping a bad photograph being taken in the first place.
Four things are measured on every frame, and the person is told about exactly one of them at a time:
Order matters
Light is resolved before distance, distance before centring, centring before angle. There is no point telling somebody to turn their head in a room too dark to see them. Only when everything before it is satisfied does the next instruction appear, so the screen never shows two problems at once.
Angles are judged per person
A fixed number for "facing forward" fails, because everybody sits differently and holds a phone differently. Instead the first shot captures that person's own neutral pose, and every later angle is measured as a deviation from their own baseline.
There is also a patience rule. If somebody cannot satisfy a pose after a few seconds, the check relaxes to accept movement on the right axis in either direction. A student stuck in a loop is a worse outcome than a slightly less varied set of templates.
The blink check learns too
Eye aspect ratio varies enormously with eye shape, camera quality and distance. A fixed threshold locks some people out completely while waving others through. The system measures the person's own open eye for about half a second, takes the median, then watches for a clear drop from that baseline. It also passes automatically after a few seconds, because nobody should be trapped at a screen blinking at their phone.
Automatic capture
There is no shutter button. Once conditions hold steady for 600 milliseconds the shot is taken. Reaching for a button while holding a pose is exactly how a blurred, badly angled template gets recorded. The ring around the circle fills as the whole capture progresses, so the person can see how much is left.
7. Database
Tables
| Table | Purpose | Columns | Rows |
|---|---|---|---|
| activity_log | Audit trail of who did what. | 6 | 291 |
| announcements | Notices shown on the student dashboard. | 6 | 2 |
| attendance | The record itself. Unique on session and student, so double marking is impossible. | 8 | 11 |
| class_sessions | One meeting of a course. Attendance hangs off a session, never off a course. | 11 | 5 |
| course_students | The class list. Attendance is only ever matched against this. | 4 | 12 |
| courses | Courses, each owned by a department and a lecturer. | 8 | 5 |
| departments | Departments, used on the sign up form and in reports. | 4 | 6 |
| face_logs | Every registration, verification and class scan, passed or failed, with the distance. | 9 | 64 |
| face_templates | One row per captured angle. Holds the 128 number embedding for that face. | 6 | 35 |
| settings | Tunable values, changed from the console rather than in code. | 2 | 9 |
| users | Everybody on the system. Students sign themselves up, staff are created by an administrator. | 15 | 13 |
The table worth explaining
face_templates is where the biometrics actually live. One row
per captured angle, each holding a JSON array of 128 floats.
| Column | Type | Notes |
|---|---|---|
| id | int(11) | Surrogate key. |
| user_id | int(11) | Cascades on delete, so removing a member removes their biometrics with them. |
| pose | enum('center','up','down','left','right') | Which angle this row came from. |
| descriptor | longtext | The 128 numbers. This is the biometric template. |
| quality | decimal(4,3) | Detector confidence at the moment of capture. |
| created_at | timestamp | When it was taken. |
Several rows per person is deliberate. Matching against three angles is far more forgiving of how somebody happens to be standing than matching against one, and the cost is a few kilobytes.
Constraints that carry real meaning
UNIQUE (session_id, user_id)onattendance. Double marking is impossible at the storage layer, not merely discouraged in code.UNIQUE (course_id, user_id)oncourse_students. A student appears on a class list once.ON DELETE CASCADEfrom a member to their templates, logs and attendance. Deleting an account really does remove the biometrics.ON DELETE SET NULLfor a lecturer on a course. Losing a member of staff must not delete the course and its history.
The schema file does not create or name a database. It builds its tables inside whichever database it is imported into, because most hosting panels choose the name for you and will not let you change it.
8. API reference
Read from the router in api.php as this page loaded, so this list
is the real one. GET reads,
POST writes and needs a CSRF token.
Account
auth.login auth.logout auth.signup auth.state
System
app.version
Face
face.enrol face.verify
Student portal
me.attendance me.courses me.dashboard me.password me.profile me.register
Recognition desk
desk.recognise desk.sessions desk.templates
Attendance records
attendance.close attendance.mark attendance.roll
Courses
courses.delete courses.list courses.register courses.roster courses.save courses.unregister
Class sessions
sessions.delete sessions.list sessions.save
Reports
reports.course
Administration
admin.announce admin.announce.del admin.facelogs admin.logs admin.member admin.member.del admin.member.face admin.member.save admin.members admin.settings admin.stats
Departments
departments.delete departments.list departments.save
43 actions in total. Every one of them calls a guard as its first statement.
9. Security
ATTR_EMULATE_PREPARES off. No string concatenation anywhere near a query.password_hash with bcrypt. Nothing is ever stored or logged in the clear.hash_equals.uploads/ and dangerous extensions are denied.includes/ is blocked from the web, so config.php is unreachable.Biometric privacy
Worth stating plainly, because it is the part people ask about. What is stored is a 128 number embedding, not an image. An embedding is not reversible into a recognisable photograph, and it is useless outside this system. Deleting an account cascades to the templates.
An administrator can reset any member's face, which deletes their templates and forces registration again on next sign in. That is the recovery path when somebody genuinely cannot get in, and it is logged.
Known trade-offs
- The class desk downloads the templates of that course's class list to the browser so matching can run at camera speed. They are embeddings rather than photographs, the desk is operated by staff, and the server re-verifies before writing. On a larger deployment this would move server side.
- The blink check passes automatically after a few seconds. That is a deliberate choice of usability over strictness for a student facing system.
- There is no rate limiting on sign in beyond the face attempt lockout. On a public deployment that would need adding at the web server.
10. Infrastructure
/admin is a real folder, so it works on both without rewrite rules.?v=2.4, so a new release can never be served from a stale cache.Server layout
/var/www/site
├── index.php gate
├── portal.php student portal
├── admin/ staff console
├── api.php the only endpoint
├── docs.php this page
├── check.php installation diagnostic
├── includes/ blocked from the web
├── assets/ css, js, recognition library
├── models/ the seven model files
└── uploads/ face thumbnails, no execution
nginx
root /var/www/site;
index index.php;
location / { try_files $uri $uri/ /index.php?$query_string; }
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
location ^~ /includes/ { deny all; return 404; }
location ~* /uploads/.*\.(php|phtml|phar)$ { deny all; return 404; }
Permissions
chown -R www-data:www-data /var/www/site
find /var/www/site -type d -exec chmod 755 {} \;
find /var/www/site -type f -exec chmod 644 {} \;
chmod -R 775 /var/www/site/uploads
Working with no internet
The recognition library and all seven model files are served from this server, not from a content delivery network. Nothing is fetched from a third party at runtime, so the system works on an isolated network and cannot be broken by somebody else's outage during a demonstration.
11. Performance
The architecture scales in the direction that matters for a school. Adding students adds rows, not server load, because the expensive part of recognition runs on whatever device is in front of the camera. The one query that grows with enrolment is the one to many search, and that is already bounded by the class list rather than the whole institution.
12. Limits and future work
Stating these plainly is more useful than pretending they do not exist.
What it does not do
- No depth sensing. The blink check defeats a printed photograph but not a high quality video held up to the camera. Real defence against that needs an infrared or depth camera, which a browser cannot reach.
- Accuracy varies with conditions. The model is strong, but a face in near darkness, at a sharp angle, or heavily obscured will fail to match. The coaching exists to prevent those inputs, and the manual override exists for when prevention was not enough.
- One camera at a time. There is no support for several doors feeding one session simultaneously. Nothing in the schema prevents it, the interface simply does not offer it.
- No password reset by email. An administrator sets a new password. Adding email delivery would mean a mail service and its configuration.
Where it would go next
- Move one to many matching to the server once class sizes make shipping templates to the browser undesirable.
- Timetable import, so sessions are generated from a schedule rather than created by hand each week.
- Notifications to a student who falls below the attendance threshold, while there is still time to do something about it.
- A trained liveness model, rather than a blink heuristic, if the deployment ever needs to resist deliberate attack rather than casual proxy attendance.