Households with personal links derived from a secret and stored as a hash¶
Context and Problem Statement¶
Guests answer without an account. A couple invites families and couples rather than single people, and one person usually answers for everyone at the same address. Each household needs a link that nobody can guess, that only shows and changes that household, and that the couple can copy again at any time (for a reminder on WhatsApp weeks later). A database dump alone must not hand out working links.
Considered Options¶
- A random token per household, stored in clear
- A random token per household, stored only as a hash (like magic links), shown once
- A token derived from the household id with a server secret (HMAC), stored only as a hash
Decision Outcome¶
Chosen option: "HMAC token, stored as a hash".
- The guest list is made of households (name, invited events, message, household poll answers,
responded_at) with guests (name, child, attendance, events, meal, allergies, person poll answers) and guest requests (people a household asks to bring; pending, added or declined). All of it hangs off the website withON DELETE CASCADE. - The link is
/antwort/<token>withtoken = base64url(HMAC-SHA256(APP_SECRET, "household:" + id))(43 characters, 256 bits). The database stores onlysha256(token)and looks households up by it. The couple's page recomputes the token, so "Link kopieren" works any time. - The guest form takes the household only from the token. Person, event, meal and poll ids in an answer are checked against that household and website; anything else is rejected.
- Submissions are rate-limited per IP and per link. The page sends
Referrer-Policy: no-referrerandX-Robots-Tag: noindex, sets no cookies and loads nothing from third parties. APP_SECRETis new. Without it, or with the.env.exampleplaceholder, the app logs a warning and falls back to a built-in value, so an upgraded instance keeps working; operators must set their own.
Consequences¶
- Good, because a stolen database contains no usable links and the couple never has to "regenerate" one.
- Good, because the same derivation needs no extra column and no extra lookup.
- Bad, because changing
APP_SECRETinvalidates every link already sent. A single leaked link is replaced by deleting and re-adding the household (a per-household link version can be added later).