Profile Schema
Status Note
This doc captures the durable profile schema foundation from #9 through #13, plus later extensions in #22, #23, #25, #26, #30, #31, #32, #33, #82, and #90.
The schema is intentionally narrow. It establishes one shared profiles table for people and communities plus first-slice account ownership, claim request, verification attempt, and field visibility tables without introducing normalized link tables, asset tables, or advanced moderation workflows.
Locked Decisions
- profiles are first-class records independent from the user account that may later claim them
profileTypeis explicit and currently supportspersonandcommunitythrough a discriminated schema union- every profile has a canonical
slugthat is globally unique across people and communities - claim state, publication state, and creation provenance are separate fields
- community-submitted unclaimed records are represented by
creationSource: "community"plusclaimState: "unclaimed" - public surfacing state is separate from ordinary publication state so valid opt-out and suppression can hide otherwise-published profiles
- account/user ownership references live in
profileOwners; provider login alone is not ownership - most public write mutations are deferred until auth and permissions are wired;
profiles:submitCommunityProfileis the current auth-gated exception - the community submission mutation requires a Convex authenticated identity before writing
- normalized alias, asset, and rich authored block tables are deferred to later profile presentation issues
- profile outbound links are currently inline typed external links; normalized link tables remain a later scaling option
- avatar and banner fields are URL placeholders for later controlled owner or concierge inputs, not ordinary community-submitted fields
profiles Table
Core identity fields:
profileType:"person" | "community"slug: globally unique canonical URL handledisplayName: public display namesortName: normalized display-sort key for deterministic listingaliases: alternate names or searchable display variants kept inline for the first schema slicetags: flexible shared discovery tags or genres, without imposing a rigid taxonomy
Core presentation fields:
headline: optional short label or one-line positioning statementbio: optional short public bioabout: optional longer owner-authored about sectionavatarImageUrl: optional display/avatar image URL for controlled future owner or concierge inputsbannerImageUrl: optional banner image URL for controlled future owner or concierge inputsregion: optional location or scene region texttimezone: optional time zone textoutboundLinks: optional inline typed external links for owner-authored, reviewed, or partner-provided profile storefront/contact links
State fields:
claimState:"unclaimed" | "claimed_unverified" | "claimed_verified"publicationState:"draft_private" | "published"creationSource:"self" | "community" | "concierge" | "import" | "moderator"publicSurfacingState:"public" | "opted_out" | "suppressed"publicSurfacingUpdatedAt: optional timestamp for the latest public-surfacing state changepublicSurfacingReason: optional short reason for opt-out or suppression statefieldVisibility: optional per-field visibility map using"public" | "unlisted" | "private"claimedAt: optional claim timestamp, present only after claim authority is establishedpublishedAt: optional publication timestamp, present once a profile has been publishedupdatedAt: application-maintained update timestamp that every profile mutation must refreshsourceAttribution: optional inline source metadata for community-submitted records
Type-specific fields:
person.pronouns: optional short pronoun textperson.roleTags: flexible role/type tags such as DJ, VJ, host, photographer, or performercommunity.subtype: optional short subtype text such as venue, collective, brand, or agencycommunity.categoryTags: flexible category tags for community discovery and presentation
Convex automatically provides _id and _creationTime; those are not duplicated in the schema.
Ownership And Claim Tables
Convex Auth provides the users and authAccounts tables used by account and provider-link flows.
profileOwners stores durable profile authority:
profileId: profile receiving ownershipuserId: Convex Auth user that owns the profileroleKey: currently the singleton literalownerstate:"active" | "revoked"grantedByClaimRequestId: optional claim request that granted ownership
profileClaimRequests stores claim review state for Discord, VRChat, VRCLinking, and manual methods.
profileVerificationAttempts stores proof-code attempts for external proof readers. Attempts have a proof code, target type, target external id, state, expiry, and optional evidence summary.
The first automated proof reader is an adapter action configured by VRCHAT_PROOF_ADAPTER_URL; it avoids hard-coding guessed VRChat or VRCLinking API behavior into the product backend.
State Semantics
claimState describes owner authority:
unclaimed: no owner authority has been attached yetclaimed_unverified: a claimant controls the profile, but stronger verification is not completeclaimed_verified: owner control and verification are both established
publicationState describes public surfacing:
draft_private: not public and not searchablepublished: eligible for public profile pages and later discovery flows, subject to permission, trust, and opt-out rules
publicSurfacingState describes whether an otherwise-published profile is allowed to appear on ordinary public surfaces:
public: profile can appear on profile pages, search, discovery, event participant references, and linked attribution surfacesopted_out: valid owner opt-out; hide from ordinary public surfacessuppressed: moderation/safety suppression; hide from ordinary public surfaces
creationSource describes how the record entered the system. It is not an authority marker by itself; authority comes from claimState and later claim records.
fieldVisibility controls public projection surfaces:
public: direct profile page plus discovery/search/card projectionsunlisted: direct profile page onlyprivate: hidden from public projections
displayName, slug, profileType, and trust labels remain public while the profile itself is public.
Mutation Contracts
Convex schema validation cannot enforce conditional timestamp invariants, so profile mutations must preserve these application-level rules:
- set
claimedAtwhenclaimStateleaves"unclaimed" - set
publishedAtwhenpublicationStatebecomes"published" - patch
updatedAton every profile write
The first write path is profiles:submitCommunityProfile. It requires ctx.auth.getUserIdentity() to return a signed-in identity, generates the slug server-side, publishes the profile as creationSource: "community" plus claimState: "unclaimed", and stores narrow source attribution for later moderation and display decisions.
The migrations:backfillProfilePublicSurfacingState internal mutation sets missing legacy publicSurfacingState values to "public" and fills publicSurfacingUpdatedAt so previously-written profiles keep their existing publication behavior after the surfacing-state schema addition.
Deploy-time migrations use @convex-dev/migrations and are run by migrations:runAll after production function deploys when CONVEX_DEPLOY_KEY is configured.
Initial Indexes
by_slug: canonical profile lookup and mutation-enforced slug uniquenessby_profileType_publicationState: public page/discovery entry points split by person vs communityby_publicationState_claimState: public/trust filtering for later profile listsby_publicSurfacingState_publicationState: public suppression and opt-out enforcementby_claimState_profileType: moderation and claim-review flows by claim state, with optional type splittingby_creationSource_claimState: moderation and community-submitted/unclaimed review flowsby_profileType_sortName: deterministic profile listing by typeprofileOwners.by_profileId_roleKey_state: active owner singleton enforcementprofileOwners.by_userId_state: account profile ownership lookupprofileClaimRequests.by_profileId_state: profile claim review lookupprofileVerificationAttempts.by_state_expiresAt: pending proof attempt expiry scans
Implementation Boundaries
#10adds canonical slugs, validation, and uniqueness rules#11adds type-aware person/community fields and documents shared vs type-specific data#12defines read/write permission behavior#13defines claim-state transitions and trust labeling behavior#22added presentation fields and public-page rendering for avatar/banner, short bio, and longer about content#23added the authenticated community submission mutation and source attribution details#25and#26add public trust/source labeling and the first audit trail#30adds public surfacing suppression enforcement#31,#32, and#33add universal public search/discovery surfaces#82added inline typed external links for first-slice creator commerce/profile links, with publichttpsfiltering#90adds scoped vocabulary normalization for tags, roles, categories, and discovery facets#27adds field-level visibility controls#31adds public search behavior and any search-specific indexing