docs(website): unique, changeable, verifiable email addresses (engagement Phase 1b) #180
Reference in New Issue
Block a user
No description provided.
Delete Branch "docs/unique-verifiable-email"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Companion to website#167.
Records Phase 1b as built, and corrects two things the plan got wrong before anyone builds on them.
The correction that matters
Phase 1b step 2 said to pin the UNIQUE index to a case-insensitive collation "for the same reason
usernamewas". Tested against the deployment's own MariaDB 11.8, that is wrong in a way that would have destroyed data: under bothutf8mb4_general_ciand the server-defaultutf8mb4_uca1400_ai_ci,josé@x.comandjose@x.comcompare EQUAL. Every_cicollation available here is also accent-insensitive.That index would refuse the second address forever, and the de-duplication would have cleared a legitimate account's address and reported it as a duplicate that never was.
The step is rewritten to point at the generated-column design that shipped, and a new As built block carries the full reasoning, the collations that would have been right but are MariaDB 11.4+ only (so pinning one just relocates §0.6's boot failure), and — worth reading on its own — the second-order version of the same bug: written the obvious way, the de-dupe's own
LOWER(email) = LOWER(email)comparison uses the column's collation and over-folds even when the index does not. A seeded fixture caught it nullingjose@x.comas a duplicate ofjosé@x.com, one statement after the fix meant to prevent exactly that.§0.6 undercounted the blast radius
It names two callers of
isDuplicateUsername(). There are five, and the three it omits fail worse than the two it names — an invite that fails after the invitee has clicked the link and chosen a password, and two admin routes with no catch at all that turned a duplicate address into an opaque 500. Amended in place as a dated note rather than a rewrite, so the original finding stays readable as what was known on 2026-08-28.The amendment also records that the violated index name lives only in the driver's message text — and that the same message embeds the bound parameters, so on an email collision it contains the address. That is a second, independent reason those errors must never reach a client.
The rest
BACKEND_DESIGN.md— theuserstable (already stale before this PR: it predated the player-account work, showingrole ENUM('admin','editor'), a NOT NULLpassword_hash, and no email columns at all), plusemail_verificationsandemail_dedupe_report, plus the five new routes. Each column carries why, not just its type — particularly why the index is onemail_normand never onemail.UPGRADE_NOTES.md— an operator entry. This is exactly the kind of change that file exists for: nothing breaks loudly, and the affected users find out the next time they try to reset a password. It says plainly that those users are the reason the warning exists and that only the operator can tell them.api-route-inventory.json— regenerated wholesale fromserver/routes.manifest.json(203 → 210). Still has no CI gate, flagged again; it will drift again.Deliberately untouched
CLAUDE.mdstill says "identities are never auto-provisioned", whichprovisionSsoPlayercontradicts — §0.6 finding 3 already records this. Correcting it is outside Phase 1b's scope; happy to take it separately.API_V2_PLAN.md— a record of the domain split as it landed, left as written.AI disclosure
Written with Claude Code (Opus 5).
Companion to website#<pr>. Records Phase 1b as built, and corrects two things the plan got wrong before anyone builds on them. ENGAGEMENT.md - Phase 1b step 2 said to pin the index to a case-insensitive collation. Every _ci collation MariaDB offers here is also accent-insensitive, so that index would refuse jose@x.com once josé@x.com existed and the de-duplication would have cleared a legitimate account's address. The as-built block records the generated-column design that shipped instead, and the second-order version of the same bug that a seeded fixture caught in the de-dupe query itself. - §0.6 named two callers of isDuplicateUsername(). There are five, and the three it omits fail worse than the two it names. BACKEND_DESIGN.md — the users table (already stale: it predated the player account work), plus email_verifications and email_dedupe_report, and the five new routes. UPGRADE_NOTES.md — an operator entry, because the de-duplication is the kind of quiet change this file exists for: nothing breaks, and the affected users find out the next time they try to reset a password. api-route-inventory.json — regenerated from the manifest; still ungated. Co-Authored-By: Claude <noreply@anthropic.com>