Morning attendance is a traffic spike with a hard SLA. Hundreds of students pass a tablet in a short window. If recognition or writes stall, the line stops — and the CRM is wrong for the rest of the day.
The problem
A naive path — capture frame, call a heavy model, await a chatty ORM write per student — collapsed under concurrency. Offline kiosk days made it worse: devices needed to keep marking when the campus network hiccuped.
Pipeline shape
- Flutter kiosk captures frames, runs on-device gating (face present / quality), and stores pending marks in Hive when offline
- Recognition service returns a student id (or unknown) with a confidence score
- NestJS attendance API validates school/session context and writes through a hot path tuned for inserts
- Admin dashboard (Next.js) reads aggregated attendance — never the hot path
Camera → quality gate → recognize → attendance write → CRM views
↓ offline
Hive outbox → sync when online
Why the write model matters
Attendance inserts are append-heavy. We avoided N+1 ORM patterns on the critical endpoint and used parameterized SQL with the right unique constraints (student + session + date) so retries stayed safe.
Redis helped for short-lived session locks and rate limits so a stuck tablet could not flood the API.
Failure modes we designed for
- Low confidence → human confirm, do not silently invent a match
- Duplicate scans → idempotent keys, not "hope the UI prevents double tap"
- Device clock skew → server time wins for the official mark
Takeaway
Computer vision is only half the product. The attendance write path, offline outbox, and admin reads have to be designed as one system or the morning line will find the weak link.