# Design note — profile and account settings

**Dated 2026-08-05. Written before the code, per the provenance practice in
[ROADMAP.md](../../ROADMAP.md#proving-independence).**

---

## The job to be done

A member can register and post, and after that there is nothing they can change
about themselves. The `users` table has had `avatar`, `title`, `signature`,
`timezone` and `locale` columns since the first migration and **nothing has
ever been able to set any of them**. There is no way to change an email
address, and no way to change a password without going through "I forgot it".

That is the first wall somebody hits after signing up, and it is the reason
this comes before import tools and search drivers.

## Decision 1 — two pages, split by what goes wrong

**Profile** is how somebody presents themselves: avatar, title, signature,
timezone. Getting it wrong is embarrassing at worst.

**Account** is email and password. Getting it wrong locks somebody out.

They are separate pages because they deserve different friction. Changing a
signature should be one field and a Save. Changing an email should ask for the
password and then not take effect until the new address is proven to work.

## Decision 2 — a new email is not the email until it answers

Setting `users.email` straight from a form is how somebody typos their address
and discovers it six months later when they need a password reset and the
reset goes to `gmial.com`.

So: the current password is required, the new address is held as
**pending**, and a link is sent to it. Only following that link moves it into
`email`. Until then the old address still works and still receives password
resets.

A notice also goes to the **old** address, because an attacker on a borrowed
session changing the email is exactly the event the account owner needs to hear
about, and the old address is the one they still read.

The pending change expires. A link that works forever is a link that works for
whoever finds the email in two years.

## Decision 3 — changing a password signs the other devices out

Password changes ask for the current one — a borrowed desk should not be enough
— and then **drop every remember-me token**, because the usual reason to change
a password is that somebody else might have it, and leaving their "keep me
signed in" cookie alive defeats the point.

The device doing the changing stays signed in. Being logged out of the tab you
just used is confusing, and it teaches people that changing a password is a
hassle.

## Decision 4 — the avatar is its own store, not an attachment

Attachments are tied to posts, served through a token route, and deleted with
the post they belong to. An avatar is none of those things: it is shown fifty
times on a page, it wants long cache headers, and it must not vanish because a
post got tidied up.

So a separate store, reusing the same discipline the attachment store
established — **MIME sniffed from the file contents, never from what the
uploader claimed, and the saved extension derived from the sniff rather than
from the filename.** SVG stays rejected: it can carry script, there is no
sanitiser here, and an avatar is served back to every other member.

Avatars are **resized on upload** to a fixed maximum and re-encoded. That is
not only about disk: re-encoding through GD drops EXIF, and a phone photo
carries the GPS coordinates of wherever it was taken. Nobody uploading a
profile picture is choosing to publish their home address.

The old file is removed when a new one replaces it.

## Decision 5 — signatures are the same pipeline as posts, with a smaller budget

A signature is member-authored HTML shown on every post they have ever made.
It goes through exactly the sanitiser posts go through — a second, laxer path
would be the security hole — and it is capped, because a signature is a
garnish and a screen-high one ruins the thread for everybody else.

## Decision 6 — a timezone that is only stored is not a feature

`timezone` has been a column with nothing reading it. It now feeds the date
helpers, so "3 hours ago" and the exact timestamp in the tooltip are both in
the member's own time.

Guests and members who have not chosen get the site's timezone. There is
deliberately no browser-detection guess written silently into the account:
detected once at registration and then wrong forever after somebody moves is
worse than a default they can see and change.

## What this does not do

- **No username changes.** A username appears in every quote, mention and old
  URL, so changing one is a rename with consequences across the whole forum —
  it needs its own note, a history, and probably a rate limit.
- **No account deletion.** It needs a real decision about what happens to the
  posts, and "delete my account" that silently orphans four years of threads is
  worse than not offering it.
- **No locale switching.** The column is there and the strings are not; there
  is no second translation to switch to yet.
- **No avatar cropping UI.** Uploads are centre-cropped to a square. A drag-to-
  crop tool is a real piece of work and centre-crop is right for almost every
  photo somebody picks.
