Skip to content

Rename member handle slug/active_slug → username everywhere#816

Merged
wintermeyer merged 3 commits into
mainfrom
rename-active-slug-to-username
Jun 19, 2026
Merged

Rename member handle slug/active_slug → username everywhere#816
wintermeyer merged 3 commits into
mainfrom
rename-active-slug-to-username

Conversation

@wintermeyer

Copy link
Copy Markdown
Owner

TL;DR

Renames a member's handle from slug/active_slug to username across 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 keep slug (a genuinely derived slug); the generic URL-routing slug machinery keeps slug too. Breaking for /api/2.0 + agent-format consumers (handle field is now username; agent-doc schema_version 2 → 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

  • DB (lossless RENAME): users.active_slugusers.username; slug_changesusername_changes; indexes/constraints renamed to match (the users unique index too, 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 registration / notification (actor_username) / image-rebuild helpers.
  • API/JSON (breaking): handle field is username (was slug); replies/authors use author_username; agent-doc schema_version 2 → 3.
  • Feature: SlugController + /slugsUsernameController + /usernames.
  • Docs: /developers reference + data model, README, robots/llms.

Deliberately kept as slug

The URL-path-segment machinery (:slug route param, ReservedSlugs, ResolveSlug/ResolveOwnedSlug/UserResolveSlug, SlugHelpers) and a tag's slug — "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

  • Pure renames — no data moved or dropped; the migration has a full down.
  • NOT N-1 safe: the previous release still queries active_slug/slug_changes. Ship as a planned-downtime deploy.
  • mix precommit green (1922 tests, credo --strict, format, warnings-as-errors). Verified in the running app: profile/endorsers JSON now carry username, /usernames/new resolves, old /slugs/* 404s.

On merge / deploy

  • Bump mix.exs (currently 6.3.0) as part of the push to main — this is a notable breaking change.
  • Local note: I migrated the dev DB to the new schema for verification. To run main locally again, mix ecto.rollback this migration (it's reversible) or stay on the branch.

https://claude.ai/code/session_01SXxEH6QnfZbhvAWsDLtCiH

…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
@wintermeyer

Copy link
Copy Markdown
Owner Author

Data-safety verification (no data lost or deleted)

Proven, not just asserted:

1. The migration only renames. No DROP/DELETE/TRUNCATE, no column type change, no add/remove/modify, no @disable_ddl_transaction — 12 RENAME statements that run atomically in the default DDL transaction, so a failure rolls back whole (no partial/corrupt state).

2. Round-trip on the real dev DB (60,538 users, 3 username_changes). Content fingerprint md5(string_agg(id || handle order by id)):

step users fp username_changes fp
before (username schema) 7d8c7e8b…251c 850d5977…4c79
after ecto.rollback (active_slug/slug_changes) 7d8c7e8b…251c 850d5977…4c79
after re-ecto.migrate (username schema) 7d8c7e8b…251c 850d5977…4c79

Identical at every step → the up/down round-trip is byte-for-byte lossless on real data. Final schema lands on users.username + users_username_index + username_changes (pkey/fk/index renamed); the only remaining slug indexes are tags.slug and work_experiences.slug (genuinely-derived slugs, correctly kept).

3. mix precommit green (1922 tests) and verified in the running app: profile/endorsers JSON carry username, /usernames/new resolves, old /slugs/* 404s.

Documentation — fully updated

README (schema list, admin SQL, username-change feature, data export), /developers (data-model, reference, index, cookbook), llms.txt, robots.txt, and code moduledocs/comments (avatar/cover filename examples, Phoenix.Param, regenerator, post system-message handle, errors.pot rule comment). Deliberately kept as slug: the :slug URL route param, ReservedSlugs/Resolve*Slug/SlugHelpers (routing + generation, shared with tags), a tag's / work-experience's derived slug, the internal @handle search field, and gettext %{slug} placeholders.

…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
@wintermeyer wintermeyer merged commit fa5aa08 into main Jun 19, 2026
1 check passed
@wintermeyer wintermeyer deleted the rename-active-slug-to-username branch June 19, 2026 21:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant