Rename member handle slug/active_slug → username everywhere#816
Conversation
…ywhere
Members were stored as `users.active_slug` and exposed as `slug` in the API/
JSON, while the UI and every human calls it a username. This unifies the name:
the database column, the schema field, the JSON/API field, the account
functions, the change-username feature and the history table all become
`username`.
What changed:
- DB: `users.active_slug` -> `users.username` and the `slug_changes` ledger ->
`username_changes` (both via lossless RENAME, indexes/constraints renamed to
match; see the migration). The `users` unique index is renamed so
`unique_constraint(:username)` keeps mapping the "taken" error.
- Schema/code: `field :username`, `Phoenix.Param` key, `get_user_by_username`,
`update_username`, `username_changeset`, `username_change_quota`,
`username_taken?`, `UsernameChange`, plus the registration/notification/
image-rebuild helpers that named the handle.
- API/JSON (breaking): the handle field is now `username` (was `slug`); post
replies/authors use `author_username`. The agent-doc `schema_version` is
bumped 2 -> 3. The third-party API + agent-format consumers must update.
- Feature: the change-username controller/routes move from `SlugController`
/`/slugs` to `UsernameController`/`/usernames`.
- Docs: `/developers` reference + data model, README, llms/robots updated.
What deliberately keeps "slug": the generic URL-path-segment machinery (the
`:slug` route param, `ReservedSlugs`, `ResolveSlug`/`ResolveOwnedSlug`/
`UserResolveSlug`, the `SlugHelpers` slug generator) and a tag's `slug` (a
genuinely derived slug) — "slug" is the correct web term there, and it is
shared with tags. Gettext `%{slug}` interpolation placeholders are left as-is
(invisible internal names; the rendered text is the real @username).
Migration safety: pure renames, no data moved or dropped. NOT N-1 / blue-green
safe (the previous release queries `active_slug`), so this is a deliberate
planned-downtime deploy, like the UUID v7 re-key — never a casual push to main.
Claude-Session: https://claude.ai/code/session_01SXxEH6QnfZbhvAWsDLtCiH
Follow-up sweep after the active_slug -> username rename, closing the last
references that still called the member handle a "slug":
- README: drop the dropped `Slug` schema from the Accounts list (the live
handle is `users.username`; the ledger is `UsernameChange`).
- Code comments: the avatar/cover served filename embeds the username, so the
examples read `<username>-<version>-<fp>.avif`; the regenerator doc and the
listing-fields comment say username; the post system-message `handle/2`
binds `username` instead of a `slug` variable.
- errors.pot: the custom username-rule error comment points at
`username_changeset/2` / `update_username/2` (renamed).
Deliberately left as "slug" (the correct term there): the `:slug` URL route
param and its comments (a profile's path segment, shared with tags), the
`ReservedSlugs` / `Resolve*Slug` / `SlugHelpers` routing-and-generation
machinery, a tag's and a work-experience entry's derived `slug`, the internal
search query field for the `@handle` operator, and gettext `%{slug}`
interpolation placeholders (invisible; the rendered text is the real @username).
Claude-Session: https://claude.ai/code/session_01SXxEH6QnfZbhvAWsDLtCiH
Data-safety verification (no data lost or deleted)Proven, not just asserted: 1. The migration only renames. No 2. Round-trip on the real dev DB (60,538 users, 3 username_changes). Content fingerprint
Identical at every step → the up/down round-trip is byte-for-byte lossless on real data. Final schema lands on 3. Documentation — fully updatedREADME (schema list, admin SQL, username-change feature, data export), |
…nge) The member handle field in the API/JSON went from `slug` to `username` and the agent-doc schema_version from 2 to 3 — a breaking change for /api/2.0 and agent-format consumers, agreed beforehand, so this is a major bump. Claude-Session: https://claude.ai/code/session_01SXxEH6QnfZbhvAWsDLtCiH
TL;DR
Renames a member's handle from
slug/active_slugtousernameacross the database, schema, JSON/API, the change-username feature, and the docs — so the name finally matches what humans (and the UI) call it. Tags keepslug(a genuinely derived slug); the generic URL-routing slug machinery keepsslugtoo. Breaking for/api/2.0+ agent-format consumers (handle field is nowusername; agent-docschema_version2 → 3). The DB rename is lossless but not blue/green-safe, so it must ship as a deliberate planned-downtime deploy (like the UUID v7 re-key), not a casual push to main.What changed
RENAME):users.active_slug→users.username;slug_changes→username_changes; indexes/constraints renamed to match (theusersunique index too, sounique_constraint(:username)keeps mapping the "taken" error).field :username,Phoenix.Paramkey,get_user_by_username,update_username,username_changeset,username_change_quota,username_taken?,UsernameChange, plus registration / notification (actor_username) / image-rebuild helpers.username(wasslug); replies/authors useauthor_username; agent-docschema_version2 → 3.SlugController+/slugs→UsernameController+/usernames./developersreference + data model, README, robots/llms.Deliberately kept as
slugThe URL-path-segment machinery (
:slugroute param,ReservedSlugs,ResolveSlug/ResolveOwnedSlug/UserResolveSlug,SlugHelpers) and a tag'sslug— "slug" is the correct web term there and is shared with tags. Gettext%{slug}placeholders are invisible internal names (rendered output is the real@username).Safety / deploy
down.active_slug/slug_changes. Ship as a planned-downtime deploy.mix precommitgreen (1922 tests, credo --strict, format, warnings-as-errors). Verified in the running app: profile/endorsers JSON now carryusername,/usernames/newresolves, old/slugs/*404s.On merge / deploy
mix.exs(currently 6.3.0) as part of the push to main — this is a notable breaking change.mainlocally again,mix ecto.rollbackthis migration (it's reversible) or stay on the branch.https://claude.ai/code/session_01SXxEH6QnfZbhvAWsDLtCiH