Repository source: docs/specs/profile_read_contract_v1.md
Profile Read Contract V1
This document defines the retained profile and username-read behavior for the PHP rewrite.
Scope
V1 profile reads cover:
public profile pages at
/profiles/<profile-slug>self-profile reads at
/profiles/<profile-slug>?self=1username routes at
/user/<username>account-key page linkage and identity-hint-aware navigation as supporting behavior
Separate post-bootstrap profile editing remains out of scope.
Canonical Inputs
Profile reads may derive state only from in-scope retained record families:
records/identity/records/public-keys/records/posts/records/instance/where needed for shared shell or project-level facts
V1 does not require records/profile-updates/, merge records, moderation records, or thread-title updates.
Identity Model
Each public profile is anchored by one canonical
Identity-ID.The public profile slug uses the identity slug form
<scheme>-<value>.For retained scope, the canonical slug for an OpenPGP identity is
openpgp-<lowercase-fingerprint>.The identity bootstrap record is the canonical source for:
identity ID
signer fingerprint
bootstrap post/thread references
bootstrap public key text
Visible Profile Fields
Every resolved profile read must be able to surface at least:
Identity-IDcurrent visible username
fallback display label
bootstrap record ID
bootstrap post ID
bootstrap thread ID
public key material
post history summary
thread history summary
Username Rules
Username capture happens once during browser key generation/bootstrap.
If browser
prompt()is unavailable, dismissed, blank, or unusable, the bootstrap flow falls back toguest.The chosen bootstrap username becomes the initial visible username for the identity.
A later username change is available only while the
FORUM_USERNAME_CHANGES_ENABLEDflag is on (default off): a signed, same-keyset-namerecord underrecords/profile-names/supplies the visible username without changing the identity, keys or approvals (see invitation entry). Other post-bootstrap profile editing is not part of V1.
Public Profile Resolution
/profiles/<profile-slug>resolves one profile by canonical identity slug.If the slug does not resolve to a known identity, the route returns
404 Not Found.Public profile reads must be renderable from indexed profile state without request-time full-repo scans.
Standard public profile presentation
The full public key is available on the profile in an initially collapsed disclosure, using progressive disclosure guidance. The escaped, selectable ASCII-armored text retains its line breaks. Copy uses only the key text; unavailable clipboard access leaves manual selection and an actionable message. An absent key has an explicit unavailable state.
Threads and Posts are shown directly for this exact identity, whether approved or unapproved. Threads are authored roots; Posts include authored roots and replies. Neither a shared username nor bootstrap ownership grants authorship.
Both lists and their full totals exclude hidden and retracted posts, identity/bootstrap-board content and scripture-expansion records. Threads also require an unretracted, non-bootstrap thread summary. Approval events, reactions and other audit records are outside these authored-content lists.
Each list defaults to 50 items (reader limit capped at 100), ordered by creation time descending with descending post ID as a stable tie-breaker. Independent
threads_positionandposts_positionparameters preserve the other list's position; contexts bind each token to the identity, list and order. Tokens confer no access rights. Totals count all eligible rows, not the current page.Invalid positions show an error and First page recovery without silently displaying a first-page list. Empty positioned pages retain return navigation when rows remain, plus First page. Native links work without JavaScript; failed network navigation can be retried with Reload or left with Back.
Static publication contains the first page. Positioned URLs use live profile rendering; they must never receive the unpositioned artifact. Profile HTML is not an offline-reader route. Existing refresh policy remains unchanged.
Tools retains detailed approval/history and bootstrap inspection. Username aggregation remains a separate approved-user view; it is supplementary to the identity's history. Forte's existing presentation is unchanged.
Self Profile Resolution
/profiles/<profile-slug>?self=1is the self-profile variant of the same identity route.Self-profile reads may use dynamic logic initially if needed, but the route contract remains owned by PHP.
If the identity is not yet publicly materialized but the request is explicitly self-marked, the route may render an empty-state bootstrap/account view instead of
404.Self-profile rendering must keep the nav/account context distinct from the public-profile view.
Username Route Resolution
/user/<username>resolves by normalized username token, not by a separate merge record family.Username matching is case-insensitive after normalization to the route token.
If no profile currently resolves for the username token, the route returns
404 Not Found.The username route is an aggregate public entrypoint for that username token, not a distinct profile type.
All approved profiles with the matching username token are grouped together on that route.
The route shows:
the count of approved profiles using that username token
combined visible thread and post totals across those approved profiles
visible threads and posts authored by any of those approved identities
a list of the approved profile slugs that participate in the aggregate
Unapproved profiles with the same username token are shown separately and do not contribute to the combined totals.
Username Collision Handling
V1 does not merge colliding identities into one canonical identity record.
Each profile remains anchored to its own
Identity-IDand canonical/profiles/<profile-slug>route.The only "merge" behavior is read-side aggregation on
/user/<username>and/users/for approved profiles that share the same normalized username token.Approved collisions are intentionally treated as one visible user bucket for browse surfaces:
/user/<username>aggregates their visible content and lists the participating approved profiles/users/groups approved profiles byusername_tokenand sums their thread/post counts
Unapproved collisions remain separate pending profiles and are never folded into approved totals.
Collision handling remains deterministic and fully derivable from indexed profile state plus approval state.
Derived Read-Model Requirements
The indexed profile model must materialize enough data to support:
profile lookup by identity slug
username lookup by normalized username token
current visible username per identity
approved-profile aggregation by normalized username token
fallback display label per identity
post and thread counts or equivalent summaries
public-key display data
self-profile/account affordances
Rendering Requirements
Profile pages must:
use the shared page shell
preserve stable links to
/profiles/<profile-slug>and/user/<username>where availablesurface enough technical detail for key-linked identity inspection
keep public and self-profile states visually distinguishable
Out Of Scope
V1 profile reads do not require:
profile-update routes
merge record families
merge-management routes
moderation-derived profile suppression
historical username-claim browsing beyond the current visible username and deterministic collision handling