1# plyr.fm Lexicons 2 3> **note**: this is living documentation. the lexicon JSON definitions in `/lexicons/` are the source of truth. 4 5## what are lexicons? 6 7lexicons 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. 8 9for background, see: 10- [ATProto lexicon guide](https://atproto.com/guides/lexicon) 11- [ATProto data model](https://atproto.com/guides/data-repos) 12 13## our namespace 14 15plyr.fm uses the `fm.plyr` namespace for all custom record types. this is environment-aware: 16 17| environment | namespace | 18|-------------|-----------| 19| production | `fm.plyr` | 20| staging | `fm.plyr.stg` | 21| development | `fm.plyr.dev` | 22 23**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. 24 25## current lexicons 26 27### fm.plyr.track 28 29the core content record - an audio track uploaded by an artist. 30 31``` 32key: tid (timestamp-based ID) 33required: title, artist, audioUrl, fileType, createdAt 34optional: album, duration, features, imageUrl 35``` 36 37this 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. 38 39### fm.plyr.like 40 41engagement signal indicating a user liked a track. 42 43``` 44key: tid 45required: subject (strongRef to track), createdAt 46``` 47 48introduced 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. 49 50early implementation mistakenly used `app.bsky.feed.like` before being corrected to use our own namespace - a lesson in why namespace discipline matters. 51 52### fm.plyr.comment 53 54timed comments anchored to playback positions, similar to SoundCloud. 55 56``` 57key: tid 58required: subject (strongRef to track), text, timestampMs, createdAt 59optional: updatedAt 60``` 61 62introduced in november 2025. the `timestampMs` field captures playback position when the comment was made, enabling "click to seek" functionality. 63 64### fm.plyr.list 65 66generic ordered collection for playlists, albums, and liked track lists. 67 68``` 69key: tid 70required: items (array of strongRefs), createdAt 71optional: name, listType, updatedAt 72``` 73 74introduced in december 2025. the `listType` field uses `knownValues` (an ATProto pattern for extensible enums) with current values: `album`, `playlist`, `liked`. 75 76this lexicon went through several iterations: 771. initially designed specifically for playlists 782. generalized to support albums and liked collections 793. simplified to just reference any record type via strongRef 80 81### fm.plyr.actor.profile 82 83artist profile metadata specific to plyr.fm. 84 85``` 86key: literal:self (singleton - only one per user) 87required: createdAt 88optional: bio, updatedAt 89``` 90 91introduced 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". 92 93## ATProto primitives we use 94 95### record keys 96 97- **tid**: timestamp-based IDs generated by the client. used for most records where multiple instances per user are expected (tracks, likes, comments, lists). 98- **literal:self**: a fixed key for singleton records. used for profile where only one record per user should exist. 99 100### strongRef 101 102`com.atproto.repo.strongRef` is ATProto's standard way to reference another record: 103 104```json 105{ 106 "uri": "at://did:plc:xyz/fm.plyr.track/abc123", 107 "cid": "bafyreig..." 108} 109``` 110 111the 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). 112 113### knownValues 114 115rather 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. 116 117## local indexing 118 119ATProto 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: 120 121- `tracks` table indexes `fm.plyr.track` records 122- `track_likes` table indexes `fm.plyr.like` records 123- `track_comments` table indexes `fm.plyr.comment` records 124- `playlists` table indexes `fm.plyr.list` records 125 126the sync pattern: when a user logs in, we fetch their records from their PDS and update our local index. background jobs keep indexes fresh. 127 128## future: codegen from lexicon JSON 129 130currently, our Python models are hand-written to match the lexicon JSON definitions. this is error-prone. 131 132issue [#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. 133 134a Rust-based SDK for this purpose is in development. once complete, the workflow will be: 135 1361. edit lexicon JSON definitions 1372. run codegen to regenerate Python models 1383. use generated models in application code 139 140this removes the manual sync burden and enables type-safe ATProto record handling. 141 142## permission sets 143 144permission sets bundle OAuth permissions under human-readable titles. instead of users seeing "fm.plyr.track, fm.plyr.like, ..." they see "plyr.fm Music Library". 145 146### fm.plyr.authFullApp 147 148full access for the main web app - create/update/delete on all collections. 149 150### enabling permission sets 151 152set `ATPROTO_USE_PERMISSION_SETS=true` to use `include:fm.plyr.authFullApp` instead of granular scopes. 153 154**requirement**: permission set lexicons must be published to `com.atproto.lexicon.schema` collection on the `plyr.fm` authority repo (`did:plc:vs3hnzq2daqbszxlysywzy54`). 155 156see [research doc](../research/2026-01-01-atproto-oauth-permission-sets.md) for implementation details. 157 158## adding new lexicons 159 160when adding a new record type: 161 1621. create the JSON definition in `/lexicons/` 1632. add the collection to `AtprotoSettings` in `backend/src/backend/config.py` 1643. add the OAuth scope in the auth flow 1654. create database migration for local indexing 1665. implement API endpoints and sync logic 167 168see existing lexicons as templates. keep records minimal - ATProto schemas can only add optional fields after publication, never remove or change required fields.