Multi-factor attendance system that verifies a student's identity with face recognition, GPS geofencing, a device fingerprint check, and blink-based liveness detection before marking them present for a professor-created session.
- JWT login with two roles (professor / student), backed by an
app_userstable. Seeded accounts are reset on every backend startup:professor1/prof123andstudent1/stud123(backend/main.py). - Student enrollment (professor only): capture a webcam photo, extract a face embedding with InsightFace, store the photo in MinIO, and save the embedding to Postgres (
backend/routers/students.py). - Student gallery (professor only): list and delete enrolled students (
frontend/src/StudentGallery.jsx). - Session creation (professor only): course name, duration, classroom GPS coordinates, geofence radius, and an optional allowed WiFi SSID; a 6-digit OTP is generated per session (
backend/routers/sessions.py). - Session dashboard: live status, attendance records, manual close, list of the professor's own sessions and all currently active sessions.
- Four-step student attendance flow (
frontend/src/StudentAttendancePage.jsx,backend/routers/attendance.py):- Validate OTP + student email against an active session.
- Score location + device: GPS distance to the classroom via the Haversine formula (60 pts) plus a device-fingerprint sanity check (40 pts); 70+ is required to proceed.
- Liveness challenge: ~40 webcam frames are sent to the backend, which runs InsightFace's 106-point landmarks, computes eye-aspect-ratio (EAR) per frame, and requires at least one detected blink.
- Final face capture, matched against the enrolled student's stored embedding.
- Duplicate-attendance protection: a unique constraint on
(session_id, student_id)blocks marking attendance twice in the same session. - CSV and Excel export of attendance, per session or across all legacy logs (professor only), via pandas/openpyxl (
backend/services/export_service.py).
Backend (backend/requirements.txt): FastAPI, Uvicorn, OpenCV (opencv-python), NumPy, psycopg2, InsightFace + ONNX Runtime (GPU build in requirements.txt, CPU build in requirements-docker.txt), MinIO client, Pandas + OpenPyXL, Pydantic / pydantic-settings, python-jose (JWT), passlib + bcrypt.
Frontend (frontend/package.json): React 19, Vite 7, Tailwind CSS 4, React Router 7, Axios, react-webcam, react-hot-toast, lucide-react icons. (react-qr-code is listed as a dependency but isn't imported anywhere in src/ — the OTP is shown as plain text, not a QR code.)
Data / infra: PostgreSQL (pgvector/pgvector:pg16 image), MinIO (S3-compatible object storage) for student photos, Nginx as reverse proxy + static file server, Docker Compose, AWS EC2 deployment script.
Face embeddings. backend/dependencies.py::get_face_app() lazily loads InsightFace's buffalo_s model (detection, recognition, landmark_2d_106 modules only, CPU execution provider, 320×320 detection size, to fit low-memory hosts). backend/services/face_service.py::detect_face_from_base64() decodes a base64 JPEG from the browser, runs the model, and returns the first detected face's embedding as a plain list of floats.
What's actually stored in Postgres. Although the Postgres image is pgvector/pgvector:pg16, the students.embedding column is defined as JSONB (backend/migrations/000_base_schema.sql), not a vector column — no CREATE EXTENSION vector or column-type migration exists in this repo. backend/routers/attendance.py contains a _match_student_with_pgvector() helper that queries embedding <=> %s::vector (pgvector's cosine-distance operator), but it is never called from any route — it's dead code left over from an earlier design.
How a lookup actually works end to end (the live /attendance/mark-secure path):
- Student submits OTP + email; the backend looks up the matching
attendance_sessionsrow and checks it's active and unexpired. - Backend re-validates the GPS/device score (Haversine distance ≤
geofence_radius, plus the device-fingerprint check) and, ifLIVENESS_ENABLED, requiresliveness_data.passedto be true. - The submitted face image is run through InsightFace to get a live embedding.
- The backend fetches only the one student row matching the submitted email, JSON-decodes its stored embedding, and computes cosine similarity between the live and stored embeddings with plain NumPy (
np.dot(...) / (norm * norm)) — there is no nearest-neighbor search across all students in this path. - If similarity ≥
RECOGNITION_THRESHOLD(0.45) and the student hasn't already been marked present for this session, asession_attendancerow is inserted with the device/location/verification-score/liveness JSON blobs and averification_methodstring (e.g.gps+device+liveness+face).
A separate, older path (backend/routers/camera.py, backend/services/camera_service.py) streams live webcam video from the server and, for each frame, loops over every enrolled student's embedding in memory (known_faces) to find the best cosine-similarity match, logging to a legacy attendance_logs table. This path is not wired into the current frontend (the component that would use it, frontend/src/AttendanceTable.jsx, is not imported by App.jsx), so it's effectively unreachable from the UI today.
There's no .env file in the repo; backend/config.py falls back to defaults that match docker-compose.yml (admin / password123 / face_recognition for Postgres, minioadmin / minioadminpassword for MinIO), so the stack below runs without creating one.
1. Start Postgres, MinIO, and the backend:
docker compose up -d --build db minio backendThis uses the root docker-compose.yml: Postgres on 5432, MinIO on 9000/9001, backend on 8000.
2. Apply the SQL migrations (nothing runs these automatically — no migration runner or initdb.d mount exists in this repo):
docker compose exec -T db psql -U admin -d face_recognition < backend/migrations/000_base_schema.sql
docker compose exec -T db psql -U admin -d face_recognition < backend/migrations/001_add_session_tables.sql
docker compose exec -T db psql -U admin -d face_recognition < backend/migrations/002_add_app_users.sql
docker compose exec -T db psql -U admin -d face_recognition < backend/migrations/003_add_attendance_logs.sql3. Run the frontend dev server:
cd frontend
npm install
npm run devNote: frontend/src/api.js hardcodes API_BASE_URL = "https://bioattend.duckdns.org/api" — it does not read VITE_API_BASE_URL despite reading it into an unused variable. A local npm run dev frontend will call the live production backend, not your local backend container, until you edit that line.
Full stack via Docker (as deployed): docker compose up -d --build (no service names) additionally builds the frontend service from frontend/Dockerfile, which bakes in an Nginx config for bioattend.duckdns.org on ports 80/443 and expects Let's Encrypt certificates mounted from /etc/letsencrypt on the host — it will not start cleanly on a machine without those certs. docker-compose.prod.yml (with deploy/Dockerfile.web and deploy/nginx.conf) is the more portable production variant: it proxies /api/ to the backend without any hardcoded domain or SSL termination baked into the image.
assets/wireframes/*.png— three generic wireframe mockups exist, but their content (e.g. "Visitors / Active / New Signups") doesn't match BioAttend's actual professor/student flows, so I didn't use them as the top-of-README screenshot. Let me know if you'd like a real screenshot or GIF captured from the running app instead.bioattend_analytics.ipynbruns entirely on synthetic/random data generated in the notebook itself (N_STUDENTS = 120, simulated GPS distances, simulated inference latency) — it is not connected to the live database, so I did not describe it as a "live analytics" feature.