A fully static face-recognition attendance system with active liveness
detection, check-in/check-out logic, an HR auth gate, and a stats
dashboard. Face detection + 128-d embedding run in the browser with
@vladmandic/face-api; Supabase is the cloud DB + matching engine.
Hosts on GitHub Pages — zero servers.
index.html → Kiosk: liveness challenge → match → check-in/out
enroll.html → HR-gated enrollment with quality checks (3 samples)
admin.html → HR-gated dashboard: stats, date filter, CSV export
js/
config.js → all tunables (thresholds, liveness, auth)
faceEngine.js → models, webcam, descriptors, landmarks, overlay
liveness.js → blink + head-turn anti-spoofing challenge
db.js → Supabase queries + check-in/out logic + stats
auth.js → Supabase Auth gate for Enroll & Records
attendance.js / enroll.js / admin.js → page logic
supabase/schema.sql → tables + pgvector + match_face() + RLS
.github/ → Pages deploy workflow
| Enhancement | Where | Why it matters |
|---|---|---|
| 🛡️ Liveness detection | liveness.js |
Requires a blink + head-turn, so a printed photo or screen can't mark attendance. |
| 🎯 Live face overlay | faceEngine.startOverlay |
Real-time box + landmarks give users feedback while positioning. |
| 🔄 Check-in / check-out | db.nextEventType |
Auto-toggles the event based on the person's last mark. |
| 🔐 HR auth gate | auth.js |
Enroll & Records require Supabase Auth login. |
| 📊 Stats dashboard | admin.html |
Present-today, events, enrolled-staff counts + date filter. |
| ✨ Quality gates | faceEngine.getQualityDescriptor |
Rejects small/blurry/low-confidence faces at enrollment. |
- Create a project at supabase.com.
- SQL Editor → paste and run
supabase/schema.sql. - Authentication → Users → Add user — create your HR/admin login (email + password). This is what unlocks Enroll & Records.
- Project Settings → API → copy the Project URL and anon public key.
Edit js/config.js:
SUPABASE_URL: "https://xxxx.supabase.co",
SUPABASE_ANON_KEY: "eyJ...anon key...",
MATCH_THRESHOLD: 0.55, // lower = stricter
REQUIRE_LIVENESS: true, // set false to skip the blink/turn challenge
REQUIRE_AUTH: true, // set false for a no-login demo (see schema §7)Webcam needs HTTPS or localhost:
python -m http.server 8080
# open http://localhost:8080- Push this folder to a GitHub repo (
main). - Settings → Pages → Source = GitHub Actions.
- The workflow publishes to
https://<user>.github.io/<repo>/. Pages serves over HTTPS, so the camera works out of the box.
- Enroll → capture 3 quality-checked samples → each stored as
vector(128). - Kiosk → liveness challenge → best frontal descriptor →
match_face()RPC finds the nearest neighbour →nextEventType()decides check-in vs check-out → row inserted → per-person debounce prevents duplicates. - Dashboard → live stats + filterable, exportable log.
- Move matching into a Supabase Edge Function so the anon client never touches raw embeddings (strongest privacy posture).
- Replace demo RLS with least-privilege policies per role.
- Add passive liveness (texture/depth) on top of the active challenge for higher-security sites.
- Add device/kiosk allow-listing for the anon key.
- Tune
IVFFlat/considerHNSWas enrolment grows.
Face embeddings are special-category biometric personal data. Before a real rollout:
- Get explicit, informed consent and offer a non-biometric fallback.
- Publish a retention & deletion policy; honour deletion requests
(
on delete cascadealready removes embeddings when an employee is deleted). - South Africa — POPIA and Nigeria — NDPR/NDPA both treat biometrics as sensitive; document your lawful basis and keep processing records.