# plyr.fm Lexicons > **note**: this is living documentation. the lexicon JSON definitions in `/lexicons/` are the source of truth. ## 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: - [ATProto lexicon guide](https://atproto.com/guides/lexicon) - [ATProto data model](https://atproto.com/guides/data-repos) ## 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` | **important**: we never use Bluesky's `app.bsky.*` lexicons. even for concepts like "likes" that Bluesky has, we define our own (`fm.plyr.like`) to maintain namespace isolation and avoid coupling to another app's schema evolution. ## current lexicons ### fm.plyr.track 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. ### fm.plyr.like 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. ### fm.plyr.comment 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. ### fm.plyr.list 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 ### fm.plyr.actor.profile 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". ## ATProto primitives we use ### 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 `com.atproto.repo.strongRef` is ATProto's standard way to reference another record: ```json { "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 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 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. ## future: codegen from lexicon JSON currently, our Python models are hand-written to match the lexicon JSON definitions. this is error-prone. issue [#494](https://github.com/zzstoatzz/plyr.fm/issues/494) tracks building a portable lexicon-to-Pydantic codegen tool. the goal is to generate models directly from the JSON definitions in `/lexicons/`, ensuring the code always matches the schema. a Rust-based SDK for this purpose is in development. once complete, the workflow will be: 1. edit lexicon JSON definitions 2. run codegen to regenerate Python models 3. use generated models in application code this removes the manual sync burden and enables type-safe ATProto record handling. ## 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 full access for the main web app - create/update/delete on all collections. ### 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`). see [research doc](../research/2026-01-01-atproto-oauth-permission-sets.md) for implementation details. ## adding new lexicons when adding a new record type: 1. create the JSON definition in `/lexicons/` 2. add the collection to `AtprotoSettings` in `backend/src/backend/config.py` 3. add the OAuth scope in the auth flow 4. create database migration for local indexing 5. implement API endpoints and sync logic see existing lexicons as templates. keep records minimal - ATProto schemas can only add optional fields after publication, never remove or change required fields.