Skip to content

plyr.fm lexicons

note: this is living documentation. the lexicon JSON definitions in /lexicons/ are the source of truth.

lexicons are ATProto’s schema system for defining record types and API methods. each schema uses a Namespace ID (NSID) in reverse-DNS format (e.g., fm.plyr.track) to uniquely identify it across the network.

for background, see:

plyr.fm uses the fm.plyr namespace for all custom record types. this is environment-aware:

environmentnamespace
productionfm.plyr
stagingfm.plyr.stg
developmentfm.plyr.dev

plyr.fm defines its own lexicons for music-specific concepts (tracks, likes, comments, playlists) rather than reusing app.bsky.* equivalents — this keeps the schema independent and music-focused. we also write to fm.teal.* collections for teal.fm scrobble integration. at login, plyr.fm requests OAuth scopes for each collection it needs to write to (see permission sets below).

the core content record - an audio track uploaded by an artist.

key: tid (timestamp-based ID)
required: title, artist, audioUrl, fileType, createdAt
optional: album, duration, features, imageUrl

this was the first lexicon, established when the project began. tracks are stored in the user’s PDS (Personal Data Server) and indexed by plyr.fm for discovery.

engagement signal indicating a user liked a track.

key: tid
required: subject (strongRef to track), createdAt

introduced in november 2025. uses com.atproto.repo.strongRef to reference the target track by URI and CID, which is the standard ATProto pattern for cross-record references.

early implementation mistakenly used app.bsky.feed.like before being corrected to use our own namespace - a lesson in why namespace discipline matters.

timed comments anchored to playback positions, similar to SoundCloud.

key: tid
required: subject (strongRef to track), text, timestampMs, createdAt
optional: updatedAt

introduced in november 2025. the timestampMs field captures playback position when the comment was made, enabling “click to seek” functionality.

generic ordered collection for playlists, albums, and liked track lists.

key: tid
required: items (array of strongRefs), createdAt
optional: name, listType, updatedAt

introduced in december 2025. the listType field uses knownValues (an ATProto pattern for extensible enums) with current values: album, playlist, liked.

this lexicon went through several iterations:

  1. initially designed specifically for playlists
  2. generalized to support albums and liked collections
  3. simplified to just reference any record type via strongRef

artist profile metadata specific to plyr.fm.

key: literal:self (singleton - only one per user)
required: createdAt
optional: bio, updatedAt

introduced in december 2025. uses literal:self as the record key, meaning each user can only have one profile record. this is updated via putRecord with rkey=“self”.

  • tid: timestamp-based IDs generated by the client. used for most records where multiple instances per user are expected (tracks, likes, comments, lists).
  • literal:self: a fixed key for singleton records. used for profile where only one record per user should exist.

com.atproto.repo.strongRef is ATProto’s standard way to reference another record:

{
"uri": "at://did:plc:xyz/fm.plyr.track/abc123",
"cid": "bafyreig..."
}

the URI identifies the record; the CID is its content hash at a specific version. we use strongRefs in likes (referencing tracks), comments (referencing tracks), and lists (referencing any records).

rather than strict enums, ATProto uses knownValues for extensible value sets. our fm.plyr.list.listType field declares known values but validators won’t reject unknown values - this allows the schema to evolve without breaking existing records.

ATProto records in user PDSes are the source of truth, but querying across PDSes is slow. we maintain local database tables that index records for efficient queries:

  • tracks table indexes fm.plyr.track records
  • track_likes table indexes fm.plyr.like records
  • track_comments table indexes fm.plyr.comment records
  • playlists table indexes fm.plyr.list records

the sync pattern: when a user logs in, we fetch their records from their PDS and update our local index. background jobs keep indexes fresh.

permission sets bundle OAuth permissions under human-readable titles. instead of users seeing “fm.plyr.track, fm.plyr.like, …” they see “plyr.fm Music Library”.

full access for the main web app - create/update/delete on all collections.

set ATPROTO_USE_PERMISSION_SETS=true to use include:fm.plyr.authFullApp instead of granular scopes.

requirement: permission set lexicons must be published to com.atproto.lexicon.schema collection on the plyr.fm authority repo (did:plc:vs3hnzq2daqbszxlysywzy54).

permission sets are resolved by PDS servers at authorization time — the include: token is expanded into granular repo: scopes and never appears in the granted token. the authority namespace (e.g. fm.plyr) must match the requesting app’s domain.

when you sign in to plyr.fm, the app requests OAuth scopes for the collections it needs to write to:

scopepurpose
repo:fm.plyr.feed.trackcreate, update, delete tracks
repo:fm.plyr.feed.likelike and unlike tracks
repo:fm.plyr.feed.commenttimed comments
repo:fm.plyr.graph.listplaylists, albums, liked lists
repo:fm.plyr.actor.profileartist profile
repo:fm.teal.alpha.feed.playscrobbles to teal.fm
repo:fm.teal.alpha.actor.statusnow-playing status
blob:*/*upload audio and images

scopes are requested at authorization time so your PDS knows exactly what plyr.fm is allowed to do.