The threat model that shaped everything. Work-study programs typically rely on honor-system clock-in sheets or unattended kiosks — neither is hard to fake. The constraint here was that a student's phone is the scanner, not the validator, so the QR code on the physical device has to be the unforgeable element. That means the code has to be time-bound, cryptographically tied to the device's shared secret, and single-use. All the interesting design decisions follow from those three requirements.
Rotating code algorithm. Each device has a shared secret provisioned at registration. The rotating code is HMAC-SHA256(secret, floor(unix_time / 10))[0:8] encoded in uppercase base32 — not base64. The base64 choice would be wrong here: QR alphanumeric mode only allows [A-Z][0-9] plus a handful of symbols, so base64's +, /, and lowercase letters would force the QR encoder to fall back to binary mode, roughly doubling code size and risking readability on cheap displays. Base32 stays entirely within the alphanumeric alphabet. The 8-byte truncation gives 64 bits of entropy per window, which is more than sufficient against brute-force over a 10-second window.
Clock-skew tolerance vs. replay defense — these are in tension. Accepting only the current window would reject legitimate scans from students whose phone clocks are a few seconds off. The server therefore accepts the current window and the two adjacent ones (±1), giving roughly a 30-second acceptance band. That 30-second window is where a replay attack could live: capture a valid code, replay it within the window. The defense is the code_used column on the event table — before accepting any scan, the server checks whether (device_id, code) already appears in event. This works because the HMAC output is deterministic per window: the same device will emit the same code for the full 10-second window, so every code is redeemable exactly once regardless of when within that window it's first used.
Per-business direction inference. The naïve implementation would track in/out state globally per student, which breaks immediately for anyone working at two locations: a check-out at campus A would make the next check-in at campus B look like a check-out. The fix is to scope direction to the intersection of (student_id, business_id). Every event record stores business_id denormalized from the device's location — a deliberate tradeoff of a small write-time redundancy for fast, join-free reads when inferring direction. The query is just "most recent event where student_id = ? and business_id = ?"; if the last direction was in, this is an out, otherwise in.
QR scanner component choice. The obvious library for in-browser QR scanning is html5-qrcode, but it breaks under React 18 strict mode because strict mode double-mounts components during development, and html5-qrcode doesn't cleanly handle being initialized twice on the same DOM node — you get a runtime error and a stuck camera stream. The alternative is getUserMedia directly on a <video> element plus jsQR running frame-by-frame on a canvas. It's about 65 lines of code, it survives strict mode's double-mount, and camera teardown is a single stream.getTracks().forEach(t => t.stop()) call on unmount. No wrapper library needed.
Hardware simulator as a spec compliance check. The browser-based device simulator (hardware_sim/index.html) reimplements the same rotating-code algorithm in pure JavaScript using the Web Crypto API's SubtleCrypto.sign('HMAC', ...). It has no dependency on the backend code, so any mismatch between the two implementations surfaces immediately as a scan rejection. Running the simulator alongside the backend and making a successful scan end-to-end is a de-facto integration test of the cryptographic protocol.