Profile link display names: provider research
Status: research snapshot supporting approved scope, 2026-09-08, captured before implementation and live provider checks. The motivating case is three indistinguishable VRChat group links on one person's profile. Names and artwork should describe destinations; they must not imply ownership or management. BASIC accepted Q3-Q8 and explicitly included destination artwork in the first version, superseding the earlier recommendation to defer it. See the companion design for decisions and subsequent implementation evidence.
Verified provider facts
| Destination | Identification and lookup | Available display data and limits |
|---|---|---|
| VRChat group | vrchat.com/home/group/grp_… identifies a group directly. vrc.group/CODE.1234 resolves through the provider's group redirect route to that canonical ID. | Community endpoint documentation describes cookie-authenticated GET /groups/{groupId}, returning name, iconUrl, shortCode, discriminator, and privacy, alongside much broader account-context data. Use an explicit projection. Group endpoint, short links. |
| VRChat user | vrchat.com/home/user/usr_… identifies a user. vrch.at/usr_… redirects to that profile; other short-link forms can identify instances, so host detection alone is insufficient. | Community docs describe cookie-authenticated GET /users/{userId}, with displayName, iconUrl, profilePicOverride, and avatar image fields. Do not retain location, friendship, notes, or other unrelated fields. Legacy non-UUID user IDs exist and do not share all short-link behavior. User endpoint, short links. |
| Discord server invite | Candidate accepted forms: discord.gg/{code}, discord.com/invite/{code}, and legacy discordapp.com/invite/{code}. Parse the code, then request GET /invites/{code} at the fixed Discord API origin. | Official invite objects include optional partial guild data; example fields include id, name, and icon. Invites have types including guild, group DM, and friend, so require a guild object before treating the destination as a server. Expiration is represented separately from guild identity. Invite resource. |
| Discord server/channel URL | /channels/{guildId}/{channelId} contains IDs, not names or an invite code. | GET /channels/{channelId} is a separate channel resource lookup. The existing OAuth flow can read the signed-in user's partial guild list with the guilds scope, but that is not arbitrary public server discovery. Treat inaccessible targets as unresolved; do not silently widen scopes or install a bot. Channel resource, current user's guilds. |
| Discord user URL / contact | /users/{userId} is a person target, not a server invite. A plain contact handle is a different kind again. | The user resource separates username and nullable global_name; neither is a guild name. Keep contact copying independent of destination-name rendering. User resource. |
VRChat's first-party guidelines explicitly say its HTTP API has no public first-party endpoint documentation, may change without notice, and link the community documentation as unofficial. They require identifying User-Agent, appropriate caching, backoff, and jitter for scheduled requests. They prohibit collecting users' credentials/session data and acting as another user. No dependable fixed VRChat request quota was established here. VRChat Creator Guidelines, API Usage / Bots.
Discord's official Get Invite page specifies no extra permission requirement, but does not explicitly settle whether the request requires authentication. Therefore an auth-free implementation remains an integration-test question, not a verified fact from this research. Discord documents bot and OAuth bearer authentication generally. Do not make signing in or granting management permissions a prerequisite for an ordinary outbound invite link. Invite resource, authentication.
Discord rate limits include route buckets and a global limit; honor response headers and Retry-After/retry_after, rather than hard-coding route quotas. Unauthenticated requests share IP-level global limits. Excessive invalid requests can also cause temporary restriction. Rate limits.
Current repository reuse
workers/group-telemetry/vrchat-client.mjsalready supplies a service-account transport, identifying User-Agent, timeout, session-cookie rotation, provider-error categories, and retry-after parsing.getGrouphas an in-memory cache, defaulting to five minutes when a caller opts into caching. Its normalized projection contains counts/membership/join/privacy, not names or icons.findProofCodedeliberately returns only a boolean and does not retain profile text. A name resolver can reuse the transport architecture without changing proof semantics.convex/schema.tshasexternalControlProofs.assetDisplayNameandprofileExternalLinks.assetDisplayName. Those are control evidence and explicit profile associations, respectively, with their own state and provenance. They are not a general public metadata cache.communityIntegrationSettings.discordGuildNameis tied to a configured integration.convex/discordVerification.tsfetches/users/@me/guildswith OAuth bearer authentication and stores manageable guild names in control proofs.convex/profileConnections.tscopies names into associations/integration configuration. Reuse an exact asset ID only after applying the appropriate public visibility and provenance rules; never publish someone's private manageable-guild list as a side effect.convex/_vrchatIdentity.tslowercases UUID-based VRChat IDs, but its normalizer extracts the final path segment from arbitrary URL hosts. That is suitable only inside its existing constrained context, not sufficient validation for an outbound URL fetcher. It does not resolve group short codes or legacy user IDs.apps/web/src/app/_components/profile-public-page.tsxcurrently infers a Discord contact from non-default label text. Writing a fetched server name intolabelcan turn an invite into a contact-copy surface.convex/_profileLinks.tsalso supplies default provider labels during normalization, so existing nonempty labels are not reliable evidence of a deliberate owner override.
Current recommendation
- Resolve supported destinations by exact URL/ID, never by searching for a similar display name. Preserve the submitted clickable URL, especially an invite's code, while caching its resolved stable entity ID separately.
- Model destination kind and metadata separately from authored labels: provider, kind, canonical entity ID, resolved name, fetched time, and resolution state. Keep explicit owner overrides distinct. Provider names must not become handles, verified badges, profile aliases, or ownership evidence.
- Use a small shared metadata cache and asynchronous lookups when a link is added or changed. Public rendering should read available metadata without a provider call per visitor. This is a latency/rate-limit cache of observed names, not a replacement for searching VRChat or Discord. Reuse by stable entity ID, but retain per-invite resolution/expiry separately.
- Include destination artwork alongside names and provider/type identification in the first version, per BASIC's explicit direction. The static-thumbnail behavior below is a recommendation for that approved scope, not yet a separately approved appearance choice.
- Candidate freshness: refresh successful active names after about a day with jitter; use bounded retries and short negative caching for temporary failures. These intervals are product/operational choices, not provider guarantees. Preserve last-known names for transient outages; treat confirmed removal or loss of public visibility separately. Do not present stale data as a successful live lookup.
- Destination metadata lookup accepts supported destination shapes and constructs fixed provider API paths. Artwork downloads instead use the approved exact-provider-host policy on every redirect, without prescribing image paths, sizes, hashes, or signature query formats. Requests never forward credentials across redirect origins. No arbitrary Open Graph crawler is needed for this slice.
- No fetching private group membership, private Discord channels, or broad account data to decorate a public link. A public link alone does not authorize publishing metadata available only because the service account belongs to a private group.
Artwork findings and recommended behavior
Verified Discord facts: a guild invite can supply the guild ID and nullable icon hash. The official CDN format is https://cdn.discordapp.com/icons/{guild_id}/{guild_icon}.png; supported guild-icon formats include PNG, JPEG, WebP, and GIF. The size query accepts powers of two from 16 through 4096. Animated hashes can start with a_; animated WebP uses an explicit animated=true option. A PNG request at ?size=128 is the proposed still-image input, with decoding still enforcing a single frame. A null icon falls back cleanly. Invite resource, Discord image formatting.
VRChat field evidence, unofficial: the community group response documents iconUrl separately from bannerUrl. Its source schema for users lists profilePicOverrideThumbnail, profilePicOverride, currentAvatarThumbnailImageUrl, currentAvatarImageUrl, iconUrl, and userIcon. These establish candidate fields, not a guaranteed official portrait precedence. Proposed user priority: profile-picture override thumbnail, profile-picture override, current-avatar thumbnail, then current-avatar image; group priority: group icon. Do not guess that iconUrl or userIcon is interchangeable with the profile portrait without confirming representative response/UI behavior. Community group endpoint, community User schema source. VRChat itself identifies these API docs as unofficial and asks clients to cache and back off; no first-party image-field precedence was established. Creator Guidelines.
Locked decision (Q10, accepted 2026-09-08): use a small static thumbnail beside the name, retaining a separate platform/type indicator. Prefer the destination icon or portrait, not a banner crop. If absent, unavailable, or rejected, use the platform/type icon in the same footprint so text and clicking still work. Decorative thumbnails should not repeat the adjacent name to screen readers. No owner-uploaded artwork replacement or artwork-hiding control is proposed for this slice; custom text labels remain independent. Names wrap cleanly on mobile. The visual treatment is agreed; exact sizing and provider behavior remain implementation validation.
Repository reuse: profile-asset-source-import.ts already offers HTTPS-only import, public-address DNS checks, IP-pinned requests, validation at each redirect through an assertSourceUrl hook, a streamed byte limit, and a total timeout. Its defaults are broad media-kit settings (12 MB, five redirects, 30 seconds), not thumbnail targets. profile-asset-validation.ts sniffs content, bounds decoded dimensions, rejects multi-frame raster images, strips/re-encodes raster content, and generates WebP displays with Sharp. It also supports SVG and preserves source/download variants, which this narrow raster-thumbnail cache does not need. profile-asset-storage.ts supplies S3 storage and configurable cache headers. These are reuse seams, not an existing destination-artwork cache or permission to expose media-kit assets.
Approved cache approach (2026-09-08): import only public provider-returned HTTPS images, checking exact trusted provider hosts and public DNS/IP addresses at every redirect. Image paths, sizes, hashes, and signature query formats are provider-owned; the earlier path-based recommendation is superseded. Bound download size, timeout, and decoded dimensions, then validate and re-encode the image; never pass API credentials to artwork hosts. Reuse the bounded transport and raster-validation primitives, with an explicitly smaller thumbnail output. Serve the sanitized thumbnail from VRDex storage so opening profiles does not fetch provider artwork per visitor. Store source identity/hash, successful-fetch time, thumbnail reference, and failure state separately from the authored link and name. Refresh alongside daily metadata; replace the thumbnail when its source changes, preserve last-known artwork during transient failures, and remove the old artwork binding on confirmed destination change, artwork removal, deletion, or visibility loss. Do not inherit the media-kit storage helper's one-year immutable default for a publicly served URL requiring removal; choose bounded serving-cache lifetime and deletion behavior. Keep names usable when only artwork fails. Exact byte/time limits, image-host redirects, and a verified VRChat portrait priority are bounded adapter-validation work before implementation.
Settled scope and remaining evidence
Locked decisions now include VRChat people/groups and Discord server invites, view-triggered refresh of metadata at least 24 hours old, last-known names during temporary failures, distinct never-resolved fallbacks, conservative improvement of existing links, editor flags for confirmed invalid links, new resolution when destinations change, and shared naming across public profiles, editor previews, and lookup results with separate API metadata. Artwork is included. Other providers and Discord channel/user metadata are outside this initial automatic-resolution scope.
The conservative migration still needs an evidence-based classifier: recognize known generated labels, preserve distinct custom wording, leave ambiguity alone, and audit importer-generated group labels. A label that equals a platform default is not proof of its authorship. This is implementation evidence work, not a reason to ask the already-settled migration question again.
An expired/invalid invite is not a transient network failure. A code that later resolves to a different guild is a different destination: discard that code's old name binding, resolve the new guild, and do not transfer old ownership, badges, or owner overrides automatically. An old guild's metadata may still be valid for other links pointing to its stable ID. Shared caches must contain only the approved public projection; a privileged successful response alone is not proof that its fields are public. Partition or decline metadata observed only through private membership instead of letting it enter a global public cache.
Before implementation, verify the smallest representative authenticated VRChat name projection and Discord invite-auth behavior through controlled adapter tests. This research does not establish live provider availability or an already deployed resolver.