<!-- Canonical page: https://docs.todoer.pro/rebuild/slugs.html -->

# Persisted display slugs

Workspace, list and section names retain their original human-readable `title`.
Migration 022 adds a separate `entity_slugs` table; it does not rename entities,
change task references, replace IDs or change existing ID-based URLs. UI clients
can use `Store::entity_slugs(actor)` to obtain authorized ID-to-slug maps and
render `@workspace`, `#list` and `$section`. This bulk read returns only entities
the current actor can read. Direct list guests receive no parent workspace slug,
ID, scope or name. Permission revocation is effective on the next read.

The canonical base lowercases Unicode text, retains letters and numbers, changes
runs of punctuation/whitespace into one ASCII hyphen, and trims leading/trailing
hyphens. It uses no transliteration or Unicode normalization dependency. For
example `My Next_Project` becomes `my-next-project`, while `CAFÉ 東京` becomes
`café-東京`. Emoji/punctuation-only names become `untitled`. The base is limited
to 64 Unicode characters. The stored slug is at most 80 characters and contains
no whitespace. Raw titles remain available for reading and rename editors.

Workspace collisions are scoped to the initial owner's account. This allocation
scope persists when ownership changes rather than silently renaming a shared
workspace. Lists are scoped to their workspace; legacy lists without a workspace
use their initial member account. Sections are scoped to their list. When the
same base is occupied, allocation selects the first available `-2`, `-3`, …
suffix, bounded to 100,000 candidates. Renames regenerate the slug in the current
scope; moving a list recalculates it in its destination scope. IDs and ordinary
revision/CAS checks remain authoritative; slugs are display names, not new lookup
or authorization keys. Existing URLs keep working after a rename or collision.

Normal workspace/list/section create and rename operations allocate slugs in the
same write transaction. A list workspace move changes its slug scope in the same
transaction as authorization, audience acknowledgement, task history and tag
mapping. Store startup runs incremental backfill within its migration transaction;
development seed and trusted JSON/legacy imports backfill within their write
transactions too. Backfill processes missing/changed entries by stable ID,
retains an unchanged entry's suffix, and never changes raw titles or revisions.
It does not run from read operations. Delete triggers remove stale allocations.
Direct SQL fixture writes outside these entry points should call backfill before
exposing data, or reopen through Store; such writes are not application APIs.

The maps deliberately avoid adding fields to every existing DTO constructor.
All REST, CLI and MCP writes already use these shared domain operations, so a UI
rename and an agent rename produce the same persisted slug behavior. Derived
slugs can be regenerated from trusted exported business rows; stable URLs and
references continue to use their original IDs, not the derived labels.
