Ganiyat IAM ProjectTechnical documentation

How this system is built

Every layer of the project, from the neural networks running in the browser down to the file permissions on the server. The inventories, endpoints and schema below are read from the live system as this page loads.

Back to the site

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.
The problem being solved is not really "taking a register". It is that a paper register can be signed by a friend. A face cannot be handed to somebody else on the way into the hall.

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

browser→ POST api.php?action=auth.login→ password_verify→ session created→ next: verify_face
camera frame→ 3 networks in the browser→ 128 numbers→ POST face.verify→ server compares again→ portal.php opens

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

index.phpThe gate. Sign in, sign up, face registration, face verification. Nobody gets past it without a face.
portal.phpStudents only. Checks the session and the face flag server side before it renders a single byte.
admin/index.phpLecturers and administrators. A real folder, so the address is /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

LanguagePlain JavaScript, ES2017. No framework, no transpiler, no build step.
RoutingThe URL hash. #/sessions or #/roll?id=4. Back and forward work as expected.
StylingOne stylesheet using custom properties as design tokens. No Bootstrap, no Tailwind.
FontsOutfit, DM Sans and IBM Plex Mono, loaded without blocking. System fonts if they never arrive.
RenderingScreens build HTML strings and swap them into one container. Every value from the database is escaped first.

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

FileWhat it doesLinesSize
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

FileWhat it doesLinesSize
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

LanguagePHP 8.3.27, running as litespeed. Written for 8.0 and above.
Database accessPDO with prepared statements everywhere, emulation switched off so the driver really does bind.
SessionsNative PHP sessions. HttpOnly, SameSite Lax, Secure once the certificate is installed.
DependenciesNone. No Composer, no vendor folder. Only PDO, JSON and OpenSSL, all standard.
TimezoneAfrica/Lagos, set once in config so late calculations are never off by an hour.

Request handling

Every request to api.php passes the same gauntlet before a handler is called:

  1. The CSV export leaves early, because it writes its own headers.
  2. verify_csrf() rejects any POST without a matching token.
  3. The action is matched against the router. Anything unknown is a 404.
  4. The handler calls a guard: require_login, require_verified, require_staff or require_admin.
  5. Database exceptions are caught centrally. A duplicate key becomes a plain sentence rather than a stack trace.

The guards, and why there are four

require_loginA session exists. Used only by the face endpoints, which by definition run before verification.
require_verifiedA session exists and the face check passed. Everything a student can touch.
require_staffLecturer or administrator, and a lecturer must also have passed the face check.
require_adminAdministrator only. Members, settings, audit trail, face logs.

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

  1. 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.
  2. 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.
  3. 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.
Libraryface-api.js, the maintained vladmandic fork, which wraps TensorFlow.js.
Model filesSeven files, 6.7 MB in total, served from models/ on this server.
RuntimeWebGL where available, CPU otherwise. Nothing is installed on the device.
Network useNone at recognition time. No third party sees a student's face or their embedding.

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

Signing in, one to oneThe account is already known, so the live face is compared only against that person's own stored angles. Threshold 0.45.
Class desk, one to manyThe search is restricted to the class list of the selected course, never the whole database. Threshold 0.5.

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:

BrightnessAveraged across the face box only, not the whole frame, so a bright window behind somebody does not mask a dark face. Below 32 is too dark, above 238 is backlit.
DistanceFace box height against frame height. Below 0.26 is too far, above 0.92 is too close.
CentringBox centre against frame centre. Beyond 0.26 off centre, the instruction says which way to move the camera.
Head angleTwo ratios from the landmarks. How far the nose sits between the jaw edges gives the turn. Eye to nose against nose to chin gives the tilt.

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

EngineMySQL or MariaDB, InnoDB throughout for real foreign keys and transactions.
Character setutf8mb4 with utf8mb4_unicode_ci, so names with any accent or mark are stored correctly.
Tables11 tables, read live from the database

Tables

TablePurposeColumnsRows
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.

ColumnTypeNotes
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) on attendance. Double marking is impossible at the storage layer, not merely discouraged in code.
  • UNIQUE (course_id, user_id) on course_students. A student appears on a class list once.
  • ON DELETE CASCADE from a member to their templates, logs and attendance. Deleting an account really does remove the biometrics.
  • ON DELETE SET NULL for 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

SQL injectionPrepared statements on every query, with ATTR_EMULATE_PREPARES off. No string concatenation anywhere near a query.
Passwordspassword_hash with bcrypt. Nothing is ever stored or logged in the clear.
Cross site request forgeryA per session token, required on every POST, compared with hash_equals.
Cross site scriptingEvery value from the database is escaped before it reaches the DOM, on both sides.
Session fixationThe session id is regenerated at sign in.
Privilege escalationEnforced on the server. Hiding a menu item is presentation, not security.
Upload executionPHP is disabled inside uploads/ and dangerous extensions are denied.
Credential exposureincludes/ is blocked from the web, so config.php is unreachable.
EnumerationSign in gives one message whether the email or the password was wrong.

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

ServerAny Linux host with PHP 8 and MySQL. Tested on Ubuntu with nginx and php-fpm.
Web servernginx or Apache. /admin is a real folder, so it works on both without rewrite rules.
TransportHTTPS is not optional. No browser will open a camera on a plain http address.
CertificatesLet's Encrypt via certbot, renewed automatically.
Static assetsVersion stamped with ?v=2.4, so a new release can never be served from a stale cache.
DeploymentCopy files. No build, no migration runner, no service to restart beyond php-fpm.

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

First loadAbout 8 MB for the library and models, cached by the browser afterwards.
Detection rateRoughly 8 to 10 frames a second on a mid range phone, deliberately throttled so the interface stays responsive.
Embedding comparison128 subtractions and a square root. Thousands per second, so it is never the bottleneck.
Sign in checkOne to one against three stored angles. Three comparisons.
Class deskClass list size times three comparisons per frame, in the browser. A class of 200 is about 600 comparisons, a fraction of a millisecond.
Server workA verification is one indexed select and one insert. The heavy computation happens on the device, not on the server.

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.
Every threshold quoted in this document is a setting, not a constant. They are changed from the console under Settings, and the values shown above were read from the live database as this page rendered.