2.9 KiB
Context
Phase 2's alert throttle (250 m / 10-min window) blocks repeat alerts from the same spot but doesn't distinguish finders — multiple people at the same spot only get one alert, and one device can re-alert by moving around. This change adds (A) a different-finder re-alert rule and (B) a 24 h per-device block via browser fingerprinting. Anti-spam motivation mirrors the 2014 system (which tracked fingerprint + IP on every alert).
Goals / Non-Goals
Goals:
- Every distinct finder who leaves a phone number reaches the owner (within the window).
- One device cannot spam the owner more than once per 24 h.
- Minimal dependency footprint: hand-rolled fingerprint hash, no third-party JS.
Non-Goals:
- Identity-grade fingerprinting (this is anti-spam, not authentication; a determined attacker can fake signals).
- IP-based blocking (privacy + NAT false positives) — fingerprint only.
- Rate limits tied to plans/billing (Phase 5).
Decisions
-
Fingerprint = lightweight hand-rolled hash of
navigator.userAgent,language,timezoneOffset,screen WxH,platform,hardwareConcurrency. JS computes a 32-bit hash → base36 string. Alternative: fingerprintjs-lite (heavier, more robust) — deferred; the hand-rolled version is dependency-free and adequate for anti-spam; upgradeable later. -
Fingerprint block is checked FIRST in
shouldAlert(before the 10-min throttle): a fingerprint seen in the last 24 h ⇒ record-only. Rationale: the same device is the same person regardless of which location/phone they present. -
Different-finder rule lives inside the 10-min window branch: within the window, re-alert if
moved > 250 mOR (newPhone != ""ANDnewPhone != lastAlert.scanner_phone). Outside the window, alert as before. -
Contact path dedup:
FinderContactqueries the last alerted scan; within the window and same phone ⇒ store-only (no SMS). Different phone ⇒ SMS. Fingerprint present and seen within 24 h ⇒ store-only. -
Schema:
ALTER TABLE scans ADD COLUMN IF NOT EXISTS fingerprint TEXT;(idempotent, nullable). New sqlc queryGetRecentScanByFingerprint(WHERE fingerprint = $1 AND scanned_at > now() - interval '24 hours' ORDER BY scanned_at DESC LIMIT 1). -
ScanRequest gains optional
phone+fingerprint; both flow intoInsertScan.
Risks / Trade-offs
- [Fingerprint is spoofable] → accepted (anti-spam, not security); document as such.
- [Different-phone rule could alert twice from one person with two numbers] → acceptable; the owner sees both contacts and the 24 h fingerprint block caps same-device repeats.
- [Fingerprint column grows] → TEXT, nullable, negligible.
Migration Plan
make db-up(idempotent ALTER) →scans.fingerprint.- sqlc
make generate(new query + column in model). - Deploy code + JS; no data migration needed.
Open Questions
- None blocking. (24 h window is a constant; adjustable later.)