memos/docs/adr/0002-username-format-and-references.md

260 lines
15 KiB
Markdown

# ADR 0002: Username Format and References
Status: Accepted
Date: 2026-08-02
Domain glossary: [Memos domain glossary](../glossary.md)
## Context
A username is a user identifier used by account creation and updates, authentication, public user resource names, API lookup, SSO provisioning, and
references to users. Those uses must share one format rather than defining separate username-like tokens.
Memos already restricts newly written usernames to ASCII letters, digits, and hyphens, with a 36-character limit and alphanumeric ends. Existing
installations may also contain older usernames such as email addresses or values with underscores. Memos continues to read and resolve those legacy
values, but they are not valid new usernames.
Markdown mentions currently drift from the writable username format: the backend accepts up to 63 Unicode letters, numbers, and hyphens; the frontend
accepts up to 63 ASCII characters; both allow invalid username shapes; and extraction lowercases the result. Editor decoration also scans raw source in
contexts where Markdown must be opaque.
This ADR defines:
- The format and case-preservation semantics for writable usernames.
- How username references resolve.
- Markdown mention as one source form of username reference.
Changing or migrating legacy username data is outside this ADR's scope.
Username rename and reuse policy, and the durable binding of an already resolved mention across those lifecycle events, are also outside scope.
Notification timing, deduplication, access policy, self-mention behavior, and other downstream effects are unchanged and outside scope.
## Decision drivers
- Give every username write path one small, stable validation rule.
- Make a reference use the username format instead of inventing another identifier grammar.
- Preserve the spelling and case selected by the user.
- Keep username write validation, reference rendering, extraction, editor recognition, and notification resolution aligned.
- Use parsed Markdown context rather than special-case regular expressions for code, links, email addresses, and other opaque syntax.
- Keep recognition deterministic and linear in memo size.
## Terminology
**User**
: A durable Memos account identified internally by an immutable user ID.
**Username**
: A user-selected, case-sensitive public identifier used for account addressing. A username is distinct from a display name and from the user's internal
ID.
**Writable username**
: A username accepted when an account is created, renamed, or provisioned after this decision.
**Legacy username**
: A stored username that does not satisfy the writable format but remains readable and addressable for compatibility.
**Username reference**
: Source text that names a username so Memos can attempt to resolve it to a user ID. The source spelling is not itself a durable relationship.
**Mention candidate**
: A Markdown source span with the form `@` followed by a complete writable username and satisfying the mention boundary and context rules.
**Resolved mention**
: A mention candidate whose username resolves to a user under the consuming operation's existing account-status and visibility policy.
## Decision
### Writable username format
The normative grammar is:
```text
Username := Alphanumeric
| Alphanumeric UsernameCharacter{0,34} Alphanumeric
UsernameCharacter := Alphanumeric | "-"
Alphanumeric := ASCII letter | ASCII digit
ASCII letter := "A".."Z" | "a".."z"
ASCII digit := "0".."9"
```
- A username is 1 through 36 ASCII characters.
- ASCII letters, digits, and `-` are the only admitted characters.
- The first and last characters are letters or digits.
- Consecutive interior hyphens are valid; there is no additional rule limiting their number.
- A digits-only value is valid.
- Case is preserved.
Requiring alphanumeric ends also excludes values made only of hyphens and prevents a trailing hyphen from becoming an ambiguous-looking part of a
Markdown mention.
The minimum is one because Username needs no product-level namespace-scarcity policy beyond being non-empty. The maximum is 36 so every UUID value has a
canonical textual representation that fits while public URLs and reference tokens remain bounded.
The ASCII-only restriction is intentional: Username is a machine-facing public identifier used in URLs, authentication, and references. Internationalized
human-readable naming belongs to Display Name rather than expanding Username normalization and comparison rules.
Examples:
| Value | Writable | Reason |
| --- | --- | --- |
| `alice` | yes | Letters only |
| `Alice-2` | yes | Case is preserved; interior hyphen is valid |
| `1alice` | yes | A digit may begin a username that also contains a letter |
| `a--b` | yes | Consecutive interior hyphens are valid |
| `a---b` | yes | Interior hyphens have no repetition limit |
| `123` | yes | Numeric spelling has no special meaning |
| `123-456` | yes | Digits and interior hyphens satisfy the grammar |
| `00000000-0000-0000-0000-000000000000` | yes | A canonical UUID occupies 36 characters |
| `-alice` | no | A hyphen cannot begin a username |
| `alice-` | no | A hyphen cannot end a username |
| `alice_smith` | no | `_` is outside the format |
| `alice@example.com` | no | Email syntax is outside the format |
| `álîçé` | no | The writable format is ASCII |
| `张三` | no | Internationalized naming belongs to Display Name |
All account creation, username update, and automatic provisioning paths use this rule. A generic resource-name rule is not the username contract and
must not be reused as a substitute merely because its current regular expression is similar.
### Username allocation
The writable format has no reserved spellings. Values such as `admin`, `root`, `system`, `api`, `memos`, and `support` are ordinary usernames and are
available subject to the same exact-case uniqueness rule as every other value. A username does not grant authority or establish trust; those properties
come from the resolved user ID, role, and explicit product presentation. Manual registration, rename, and automatic provisioning apply the same policy.
If Memos later needs a system-owned username, the corresponding account claims it through ordinary uniqueness rather than expanding a lexical reserved
list.
SSO provisioning adopts the external identifier exactly when it is a valid, available writable username. If it is invalid or already belongs to another
user, provisioning assigns a generated canonical UUID instead. A matching username never links or takes over an account; only the persisted identity
binding formed by the identity-provider key and external subject identifies a returning SSO user.
### Username identity and resolution
Username spelling is case-preserving and username equality is exact ASCII byte equality. Memos does not lowercase, case-fold, or otherwise normalize a
username before validation, uniqueness checks, authentication, resource lookup, or reference resolution. In particular, surrounding whitespace is not
trimmed into a different username; a value such as ` alice ` is invalid. `Alice` and `alice` are distinct usernames and may identify different users.
All storage backends and application lookup paths must preserve this equality rule. A database collation that treats `Alice` and `alice` as equal, or a
lookup that silently resolves one spelling to the other, does not conform to this decision.
The `username` column therefore uses an explicit case-sensitive binary collation on every supported storage backend: `utf8mb4_bin` on MySQL, `C` on
PostgreSQL, and `BINARY` on SQLite. For the writable ASCII format, this makes exact-byte uniqueness and lookup a schema property rather than an assumption
about each database's default collation.
A username-bearing field or resource-name segment always resolves as a username, including when its spelling contains only digits. Character shape must
not select the internal user-ID namespace; an operation that addresses a user by internal ID must expose that choice separately.
The stable identity of a user is the internal user ID. A username reference is resolved as late as the consuming operation permits:
1. Preserve the username exactly as written.
2. Look up that exact username in the operation's visibility and account-status scope.
3. If it resolves, use the resulting user ID for durable effects.
4. If it does not resolve, retain ordinary source text and produce no user-targeted effect.
Product intent permits username rename and later reuse while requiring an already resolved mention not to change users. A source spelling alone cannot
satisfy all three properties: preserving the original target requires a durable binding to the user ID outside the `@username` text. This ADR does not
define that binding, historical re-resolution behavior, or the username lifecycle needed to support it.
Legacy usernames remain valid exact lookup keys for existing API resource names and authentication flows. They are not accepted by write validation and
are not added to new reference syntaxes. Changing legacy username values remains outside this decision, but every lookup preserves their stored spelling.
### Markdown mention syntax
A Markdown mention is the first defined username-reference syntax:
```text
MentionCandidate := "@" Username
```
The `Username` production is the complete writable username format above. Mention parsing has no independent character set or length limit.
Candidate recognition follows these rules:
1. The `@` is U+0040 COMMERCIAL AT in an eligible literal-source run.
2. At the start of the document, the left boundary is satisfied. Otherwise the immediately preceding source character must not be a
`UsernameCharacter`. The username format therefore defines the boundary: a preceding letter, digit, or hyphen blocks recognition.
3. The lexer consumes the complete consecutive run of ASCII letters, digits, and hyphens after `@`, then validates that whole run as a writable username.
It does not shorten an invalid run into a valid prefix.
4. GFM email-address recognition takes precedence. No substring of a recognized email address is a mention.
Characters outside `UsernameCharacter`, including `_`, `@`, and non-ASCII characters, are ordinary boundaries and receive no special handling.
The rules produce:
| Source | Mention candidate |
| --- | --- |
| `@alice` | `alice` |
| `@Alice-2` | `Alice-2` |
| `hi, @alice.` | `alice` |
| `中文@alice` | `alice` |
| `hello@alice` | none |
| `foo-@alice` | none |
| `foo_@alice` | `alice` |
| `@123` | `123` |
| `@-alice` | none |
| `@alice-` | none |
| `@alice_smith` | `alice` |
| `@alice@example` | `alice` |
| `@alice@bob` | `alice` |
| `@alice@example.com` | none |
| `@` followed by 37 valid username characters | none |
### Markdown context
Mention recognition operates on original Markdown source after GFM and Memos block and inline structure is resolved. It uses the same literal-source-run
and opaque-node model as other Memos inline extensions:
- Paragraphs, headings, block quotes, list items, table text, emphasis, strong emphasis, and strikethrough expose ordinary text.
- Code spans, code blocks, resolved links and images, autolinks, recognized GFM email addresses, raw HTML, and math are opaque.
- Escapes, character references, line endings, and syntax delimiters end a literal-source run. An escaped or character-reference-produced `@` is not an
introducer.
- Unknown future Markdown extension nodes are opaque unless their definition explicitly exposes ordinary text.
Parsing, read rendering, metadata extraction, editor decoration, and any future mention completion must use these same lexical and context rules.
This decision changes only which exact username spellings are extracted as mention candidates. Existing resolution, rendering, notification, and access
policies consume those candidates without otherwise changing behavior. Autocomplete, suggestion ranking, and a dedicated structured mention payload are
also outside this decision.
## Consequences
- Username validation becomes a domain primitive shared by every write path.
- Username uniqueness, authentication, resource lookup, and reference resolution use the same case-sensitive equality rule on every storage backend.
- Mention syntax automatically follows future username-format decisions only when this ADR is superseded and both uses are updated together.
- Existing valid mixed-case usernames can be mentioned without lowercasing.
- Values accepted only by the old 63-character or Unicode mention lexers stop being mention candidates because they were never writable usernames.
- Legacy email-like or underscore-containing usernames remain usable through compatibility lookup but cannot be authored as new Markdown mentions.
- Markdown-aware recognition removes false mentions from code, links, email addresses, HTML, math, escapes, and character references.
- Username rename, reuse, and historical mention stability remain an explicit follow-up because source text alone cannot represent the intended stable
user binding.
## Alternatives considered
### Define mention syntax first
Rejected because it creates a second username-like grammar and allows account validation and references to drift again.
### Allow a broader mention token than writable usernames
Rejected because such candidates cannot reliably identify newly writable accounts and complicate boundaries, validation, and UI messaging.
### Store stable user IDs with a mention
Deferred. Stable IDs make renames and username reuse robust but require either a new source representation or persisted occurrence bindings, plus editing
UX, import/export behavior, and compatibility policy. Exact username references are sufficient for candidate recognition in the current scope, but do not
solve historical binding.
### Unicode usernames
Deferred. A safe Unicode identifier design needs a pinned Unicode version, normalization, script and confusable policy, and consistent database collation.
ASCII keeps this decision small and matches the existing writable format.
## References
- [GitHub Flavored Markdown 0.29-gfm](https://github.github.com/gfm/)
- [GitHub: Mentioning people and teams](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#mentioning-people-and-teams)
- [Slack message formatting: mentioning users](https://docs.slack.dev/messaging/formatting-message-text/#mentioning-users)
- [Matrix client-server API: user and room mentions](https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions)
- [Activity Streams 2.0 Vocabulary: Mention](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-mention)
- [Unicode Standard Annex #31: Unicode Identifiers and Syntax](https://www.unicode.org/reports/tr31/)
- [Unicode Technical Standard #39: Unicode Security Mechanisms](https://www.unicode.org/reports/tr39/)