Skip to content

ADR-0026: Deterministic Daily Codes for Offline Check-in/Check-out

  • Status: Accepted
  • Date: 2026-07-16
  • Deciders: User + agent grill session
  • Issues: #144

Context

Issue #144 introduces a check-in/check-out flow with QR codes printed on thermal labels at the church reception desk. The check-in replaces the existing classroom attendance marking (Chamador → receptionist). Each check-in generates a random code that appears on the label and on a parent tear-off stub; at check-out, the receptionist verifies the stub code before releasing the child.

The target church (IJCP) routinely experiences internet outages. Check-in must continue to function when the Cloudflare Worker backend is unreachable. However, the admission code must still be unique per child, per session, per day — two children cannot receive the same code, even if two receptionists are checking in simultaneously from different machines.

Design constraints from the grill session

  • Per-session check-in (not per-day). Children attending two Sunday sessions have two independent check-in records.
  • Per-session daily code (not per-day). Each session gets a different code.
  • QR on the label encodes {studentId, sessionId, dailyCode} as JSON.
  • Parent stub shows the code in plain text (no QR).
  • Check-out verification: scan QR + type code from parent stub → system validates match.
  • Wrong code attempts are audited per-attempt (event-sourced).
  • Church configurable: pick-up mode, code attempt limit, emergency override policy.
  • Permanent student ID QR ("carteirinha") is a separate future feature.

Decision

Daily codes are deterministic, derived via HMAC, not randomly generated and stored.

daily_code = base32( HMAC-SHA256(church_secret, student_id + session_id + date) )[0:6]

The church_secret is a per-church random string stored in church_settings.checkin_secret. The client fetches it once when online and caches it in the browser. Both client and server can compute the same code independently, without a code storage table.

Key properties

PropertyValue
AlgorithmHMAC-SHA256, truncated to base32 first 6 chars (uppercase, excluding 0/O/1/I/L/8/B)
SecretPer-church, stored in church_settings.checkin_secret
Deterministic inputsstudent_id + session_id + date (YYYY-MM-DD)
Code length6 characters, ~729M combinations (30^6 with ambiguous chars excluded)
ExpiryImplicit — date changes, code changes
Offline supportClient caches secret; computes code locally with no server call
Collision riskZero — deterministic, no randomness
Security modelCode = possession token. Knowing the derivation scheme doesn't help without the secret. Predictable only to someone with the secret.

Why not randomly generated codes in D1?

  • Offline impossible. Code uniqueness requires a central authority (D1). Without connectivity, two clients could generate the same random code.
  • Storage cost. A separate daily_codes table with one row per child × session × day, pre-generated or created at check-in time, with expiry logic.
  • Label reprint. If a code is generated server-side and the printer jams, the code already exists — reprinting is safe. But the whole flow depends on a round-trip to D1.

Deterministic codes eliminate the storage problem and work offline with a cached secret. The trade-off is that code disclosure + secret compromise allows an attacker to derive any child's code for any day. We accept this because the threat model is a church reception desk, not a financial system.

Consequences

Positive

  • Offline check-in works. The client caches the secret once; all code generation is local.
  • No daily_codes table. No pre-generation, no expiry cron, no cleanup.
  • Label reprint is trivial. Same inputs → same code. Just re-print.
  • Codes are not stored in the database. Even if check_in_events is compromised, the codes are only present for completed CHECK_IN rows — an attacker can't predict future codes without the secret.

Negative

  • Secret rotation invalidates all active codes. If the admin rotates the secret, all labels for the current day must be reprinted. Rotation should be a rare, deliberate operation.
  • Secret cached client-side. A compromised browser could leak the secret. Mitigated by the fact that the secret is church-specific and only useful within that church's check-in context.
  • No per-code expiry. The code is valid for the entire day. There is no "this code was used at 9:00, can't be re-used at 10:00" — but the partial unique index on (student_id, session_id) prevents double check-in.

Alternatives Considered

A — Random codes stored in D1 (daily_codes table)

Pre-generate codes for each child × session at start of day. Check-in activates a pre-existing code. Requires D1 connectivity. Rejected because offline is a hard requirement.

B — Random codes generated client-side with sync

Client generates random 6-char code, syncs to D1. On collision, server regenerates and client reprints. Complex, error-prone, and the label reprint on collision is a bad UX. Rejected.

C — Sequential codes (per-session counter)

CODE-001, CODE-002 per session. Predictable and guessable — a parent could derive their neighbor's code. Rejected.

References

Distribuído sob licença MIT.