Facial Recognition Attendance System
A four-service attendance platform - a React frontend, a Node.js orchestration backend, a dedicated MongoDB microservice, and a Flask computer-vision service - that only records attendance once a live snapshot clears an 80% match rate against a student's stored reference photos.
Why it exists
A term project for a computer vision course, built with a team of five: replace manual roll call and card swipes with a system that recognizes a student's face and records attendance on its own - while taking seriously, from the proposal onward, that faces get misidentified, lighting changes, and a photo held up to a webcam shouldn't be able to fool it.
Four services, one system
Nothing shares a codebase or a database connection - each piece only knows the others through an authenticated API call:
Frontend
React + TypeScript, Firebase Auth
Professor login, student roster, live webcam capture
Backend
Node.js + Express
Orchestrates every request - never touches MongoDB directly
Database
Node.js + Express + MongoDB
Students, courses, attendance records - Dockerized
CompVision
Flask + TensorFlow
Face-embedding model behind a single /predict endpoint
That strict split has a real cost: the backend can't just join across tables. Building a professor's full attendance report means walking courses → students → attendance records → timestamps as a chain of separate API calls, because no single service is allowed to see the whole picture at once.
Signing in, for real
Two systems handle two different jobs here, and they don't actually know about each other:
Firebase Auth → decides who can log in. Real email/password check, live session state.
Backend Authorization header → one shared string, hardcoded in the frontend, identical for every professor.
Managing the roster
Taking attendance
1. Enter a 9-digit student number + course code
2. Capture Photo - grabs a frame from the live react-webcam feed
3. Process Photo - base64 to Blob, sent as multipart form data
4. Snackbar reports the result: recorded, or not a strong enough match
The match isn't one photo vs. one photo
A captured snapshot is compared against every reference photo on file for that student, not just one - each pair gets its own embedding distance from the model, and attendance only gets recorded if more than 80% of those comparisons agree it's a match:
A single lucky (or unlucky) frame can't decide it either way - a real answer to the proposal's own worry about misidentification from a single bad angle or lighting condition.
Inside the CompVision service
The model teammates built is a Siamese network - a custom Keras L1Dist layer computes the absolute difference between two face embeddings. In practice /predict resizes both images to 100x100, runs each through the model to get its own embedding, then takes a plain Euclidean distance between the two and calls anything under 14 a match - a simpler decision rule than training the network's own distance head to make that call. Packaging it was its own fight: the Docker image installs TensorFlow before anything else in requirements.txt, and needs build-essential, libhdf5-dev, and a Fortran compiler just to get h5py to build.
What I built, and what I didn't
I led the frontend, backend, and database services end to end - the architecture above, the matching logic, the API contracts between all four services, and wiring the whole thing into one working product. The face-embedding model itself came primarily from teammates on the five-person team.
What's honestly still rough
Two disconnected auth systems
Firebase genuinely decides who can log in - real email/password checks, live session state via onAuthStateChanged. But every API call to the backend carries one shared string, hardcoded in the frontend source and stored in localStorage, checked against a single static key. Firebase and the backend's own authorization never actually talk to each other.
Signing up never creates a Professor record
The database service has a full Professor model and a createProfAccount endpoint ready to go. The frontend's signup flow just never calls it - professorEmail gets used everywhere as a plain string key instead.
The database service checks presence, not identity
Its authorization middleware only checks that an Authorization header exists at all, not that it matches anything specific - it trusts that only the backend can reach it on the network, rather than verifying that directly.
The original proposal called for Flask or Django on the backend with plain OAuth2/JWT auth - in practice, Node.js/Express carried the backend and database services, and Firebase Auth handled login, with Flask kept only for the one service that actually needed Python: the computer-vision model itself. Of the services I owned, only the database is Dockerized to match CompVision's container - the frontend and backend still just run directly through Node.