Files
docs/website/UPGRADE_NOTES.md
wtclaude 84481ee6b1 docs(website): unique, changeable, verifiable email addresses (engagement Phase 1b)
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>
2026-08-29 01:54:05 -05:00

7.6 KiB

Upgrade notes — website

Operator-facing. Newest first. One entry per change that requires an operator to do something, or that changes behaviour they would otherwise discover by accident.

Most upgrades need nothing: the schema is replayed on every boot with IF NOT EXISTS migrations, and a deployment that pulls a new image and restarts is done. This file exists for the exceptions — and the exceptions worth writing down are almost always the ones that fail quietly, because a loud failure announces itself and a quiet one does not.

Read the entries between the version you are on and the version you are moving to. Each names the action, whether it is required, and what happens if you do nothing.


Email addresses are now unique, and some accounts may lose theirs

Required only if the dashboard says so. Engagement Phase 1b.

What changed

An account's email address is now the destination for account recovery and, in time, for notification mail, so an address may belong to one account only. Until now it could repeat — SSO sign-up wrote whatever address the provider returned, without checking — so a database that has been running with SSO enabled may well hold duplicates.

Users can now also set and change their own address, under Account → Email address, confirming it by opening a link. Before this there was no way for anyone to change their own address at all.

What the upgrade does on first boot

Nothing you need to trigger, and no account is deleted. Where several accounts share an address:

  • the earliest-created account keeps it — not the "verified" one, because SSO used to mark an address verified merely for existing, so that flag cannot arbitrate anything;
  • every later account has its address cleared (set to nothing) and is listed in a report;
  • the dashboard then shows a warning naming how many accounts were affected.

Case is folded — Foo@x.com and foo@x.com are the same mailbox — but accents are not: josé@x.com and jose@x.com are correctly treated as two different addresses.

What you have to do

If the warning appears, open Admin → Users and read the report. It names each affected account and the address it lost. Those users are the reason this warning exists: they can still sign in normally, but they can no longer receive password-reset or notification email until they set a new address themselves, and nothing tells them that. Contact them, and point them at Account → Email address.

Dismissing the warning keeps the report — it is the record of what the upgrade did, and the only place the lost addresses survive.

Verification email

Confirmation links are sent through the same transport as everything else, so if outbound email is not configured, nobody can confirm an address. The screen says so honestly rather than claiming a mail was sent. If you have not configured SMTP yet, see the entry below first.

The verification setting

A new setting, require a confirmed address before sending notification email, is seeded off on an existing deployment and on for a fresh install. The asymmetry is deliberate: switching it on retroactively would silently stop mailing every user who had already opted in, on the day you upgraded. Nothing reads it yet — it takes effect when engagement notifications ship — so there is no hurry to change it.

If you do nothing

The de-duplication has already run; it runs on the first boot whether or not you read this. What you lose by ignoring it is the chance to tell the affected users, who will otherwise discover it the next time they try to reset a password.

Data

Nothing is dropped and no row is deleted. users gains email_norm (a generated lowercase copy of email, which carries the uniqueness index) and email_pending. Two tables are added: email_verifications and email_dedupe_report. Cleared addresses survive in the report.


Outbound email: the Gmail connect flow is gone; configure SMTP

Required, if this deployment currently sends email. Engagement Phase 1.

What changed

The "Connect Gmail" consent flow has been removed. Email now sends through an ordinary mail transport whose credentials an admin types in — SMTP is the baseline and the only transport shipped.

Removed with it: GET /api/v1/admin/email/connect/start, GET /api/v1/admin/email/connect/callback, and email's reuse of the google authentication provider's client id and secret. That reuse is worth a sentence of its own, because it was a coupling nobody could see: rotating the Google SSO secret silently broke outbound mail, with nothing on either screen relating the two. Single sign-on is otherwise unaffected — the google provider row exists for SSO in its own right and is untouched.

What you must do

On the first boot after the upgrade, email_config.transport backfills to smtp with no credentials. Until you supply some, no mail is sent — and nothing errors, because every sender is written to degrade politely:

  • the contact form falls back to a mailto: link,
  • an invite returns the accept link for you to send by hand,
  • a password reset still answers its usual generic success,
  • Team notifications are logged and dropped.

That is the risk this note exists for: email stops and nothing complains. The admin dashboard shows a warning while it is true, and the warning goes away by itself once a credential is saved.

Go to Admin → Settings → Email, choose the posture that fits, then use Send test — which is now the real verification of the whole configuration, not a formality.

Posture Settings Notes
A relay — recommended The relay's host, port 587, Implicit TLS off, your API key as the password Mailgun, SES, Postmark and equivalents. Reputation, DKIM and bounce handling are the relay's problem. The right answer for anything with volume
Gmail over SMTP — the shortest migration smtp.gmail.com, port 587, Implicit TLS off, your Google address as the username, and an app password This is the direct replacement for the removed connect flow. It needs an app password — 2-Step Verification must be on, and your ordinary account password will not work. Google's per-day send caps apply
Your own MTA Its host, port 25, Implicit TLS off, username and password left blank For an operator already running mail on the same host

Two fields cause most failures:

  • Implicit TLS is on only for port 465. On port 587 leave it off — the connection still upgrades to TLS, via STARTTLS. Port 587 with it on does not error; it hangs.
  • Send from is now typed by you rather than read back from Google, so nothing guarantees the server will accept it. It must be an address the account is allowed to send as, or the mail is refused for SPF/DMARC reasons that look like nothing at all. Send test names this specifically when it happens.

If you do nothing

Mail stays off. Nothing breaks and no data is lost — the site, the admin panel, logins, SSO and the shard integration all behave exactly as before. The dashboard warning stays up until you either configure a transport or press Clear credentials, which is how you say "this deployment deliberately sends no mail".

Data

Nothing is dropped. email_config gains transport, credential_enc and reply_to; the old provider and refresh_token_enc columns are kept but no longer read. Your stored Gmail refresh token is not used again and is cleared the first time you press Clear credentials.