plyr.fm lexicons
note: this is living documentation. the lexicon JSON definitions in
/lexicons/are the source of truth.
what are lexicons?
Section titled “what are lexicons?”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:
our namespace
Section titled “our namespace”plyr.fm uses the fm.plyr namespace for all custom record types. this is environment-aware:
| environment | namespace |
|---|---|
| production | fm.plyr |
| staging | fm.plyr.stg |
| development | fm.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).
current lexicons
Section titled “current lexicons”fm.plyr.track
Section titled “fm.plyr.track”the core content record - an audio track uploaded by an artist.
key: tid (timestamp-based ID)required: title, artist, audioUrl, fileType, createdAtoptional: album, duration, features, imageUrlthis 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.
fm.plyr.like
Section titled “fm.plyr.like”engagement signal indicating a user liked a track.
key: tidrequired: subject (strongRef to track), createdAtintroduced 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.
fm.plyr.comment
Section titled “fm.plyr.comment”timed comments anchored to playback positions, similar to SoundCloud.
key: tidrequired: subject (strongRef to track), text, timestampMs, createdAtoptional: updatedAtintroduced in november 2025. the timestampMs field captures playback position when the comment was made, enabling “click to seek” functionality.
fm.plyr.list
Section titled “fm.plyr.list”generic ordered collection for playlists, albums, and liked track lists.
key: tidrequired: items (array of strongRefs), createdAtoptional: name, listType, updatedAtintroduced 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:
- initially designed specifically for playlists
- generalized to support albums and liked collections
- simplified to just reference any record type via strongRef
fm.plyr.actor.profile
Section titled “fm.plyr.actor.profile”artist profile metadata specific to plyr.fm.
key: literal:self (singleton - only one per user)required: createdAtoptional: bio, updatedAtintroduced 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”.
ATProto primitives we use
Section titled “ATProto primitives we use”record keys
Section titled “record keys”- 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.
strongRef
Section titled “strongRef”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).
knownValues
Section titled “knownValues”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.
local indexing
Section titled “local indexing”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:
trackstable indexesfm.plyr.trackrecordstrack_likestable indexesfm.plyr.likerecordstrack_commentstable indexesfm.plyr.commentrecordsplayliststable indexesfm.plyr.listrecords
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
Section titled “permission sets”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”.
fm.plyr.authFullApp
Section titled “fm.plyr.authFullApp”full access for the main web app - create/update/delete on all collections.
enabling permission sets
Section titled “enabling permission sets”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.
login scopes
Section titled “login scopes”when you sign in to plyr.fm, the app requests OAuth scopes for the collections it needs to write to:
| scope | purpose |
|---|---|
repo:fm.plyr.feed.track | create, update, delete tracks |
repo:fm.plyr.feed.like | like and unlike tracks |
repo:fm.plyr.feed.comment | timed comments |
repo:fm.plyr.graph.list | playlists, albums, liked lists |
repo:fm.plyr.actor.profile | artist profile |
repo:fm.teal.alpha.feed.play | scrobbles to teal.fm |
repo:fm.teal.alpha.actor.status | now-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.