···11+# TypeSpec to Lexicon Reference
22+33+This guide maps atproto Lexicon JSON syntax to TypeSpec (Tylex). It assumes you're familiar with Lexicon and want to understand how to express it in TypeSpec.
44+55+## Quick Start
66+77+Every TypeSpec file starts with an import and namespace:
88+99+```typescript
1010+import "@tylex/emitter";
1111+1212+/** Common definitions used by other lexicons */
1313+namespace com.example.defs {
1414+ // definitions here
1515+}
1616+```
1717+1818+**Maps to:**
1919+```json
2020+{
2121+ "lexicon": 1,
2222+ "id": "com.example.defs",
2323+ "description": "Common definitions used by other lexicons",
2424+ "defs": { ... }
2525+}
2626+```
2727+2828+Use `/** */` doc comments for descriptions (or `@doc()` decorator as alternative).
2929+3030+## Top-Level Lexicon Types
3131+3232+### Query (XRPC Query)
3333+3434+```typescript
3535+namespace com.example.getRecord {
3636+ /** Retrieve a record by ID */
3737+ @query
3838+ op main(
3939+ /** The record identifier */
4040+ @required id: string
4141+ ): {
4242+ @required record: com.example.record.Main;
4343+ };
4444+}
4545+```
4646+4747+**Maps to:** `{"type": "query", ...}` with `parameters` and `output`
4848+4949+### Procedure (XRPC Procedure)
5050+5151+```typescript
5252+namespace com.example.createRecord {
5353+ /** Create a new record */
5454+ @procedure
5555+ op main(input: {
5656+ @required text: string;
5757+ }): {
5858+ @required uri: atUri;
5959+ @required cid: cid;
6060+ };
6161+}
6262+```
6363+6464+**Maps to:** `{"type": "procedure", ...}` with `input` and `output`
6565+6666+### Subscription (XRPC Subscription)
6767+6868+```typescript
6969+namespace com.example.subscribeRecords {
7070+ /** Subscribe to record updates */
7171+ @subscription
7272+ op main(cursor?: integer): (Record | Delete);
7373+7474+ model Record {
7575+ @required uri: atUri;
7676+ @required record: com.example.record.Main;
7777+ }
7878+7979+ model Delete {
8080+ @required uri: atUri;
8181+ }
8282+}
8383+```
8484+8585+**Maps to:** `{"type": "subscription", ...}` with `message` containing union
8686+8787+### Record
8888+8989+```typescript
9090+namespace com.example.post {
9191+ @rec("tid")
9292+ /** A post record */
9393+ model Main {
9494+ @required text: string;
9595+ @required createdAt: datetime;
9696+ }
9797+}
9898+```
9999+100100+**Maps to:** `{"type": "record", "key": "tid", "record": {...}}`
101101+102102+**Record key types:** `@rec("tid")`, `@rec("any")`, `@rec("nsid")`
103103+104104+### Object (Plain Definition)
105105+106106+```typescript
107107+namespace com.example.defs {
108108+ /** User metadata */
109109+ model Metadata {
110110+ version?: integer = 1;
111111+ tags?: string[];
112112+ }
113113+}
114114+```
115115+116116+**Maps to:** `{"type": "object", "properties": {...}}`
117117+118118+## Reserved Keywords
119119+120120+Use backticks for TypeScript/TypeSpec reserved words:
121121+122122+```typescript
123123+namespace app.bsky.feed.post.`record` { ... }
124124+namespace `pub`.leaflet.subscription { ... }
125125+```
126126+127127+## Inline vs Definitions
128128+129129+**By default, models become separate defs.** Use `@inline` to prevent this:
130130+131131+```typescript
132132+// Without @inline - becomes separate def "statusEnum"
133133+union StatusEnum {
134134+ "active",
135135+ "inactive",
136136+}
137137+138138+// With @inline - inlined where used
139139+@inline
140140+union StatusEnum {
141141+ "active",
142142+ "inactive",
143143+}
144144+```
145145+146146+Use `@inline` when you want the type directly embedded rather than referenced.
147147+148148+## Optional vs Required Fields
149149+150150+**In lexicons, optional fields are the norm.** Required fields are discouraged and need explicit `@required`:
151151+152152+```typescript
153153+model Post {
154154+ text?: string; // optional (common)
155155+ @required createdAt: datetime; // required (discouraged, needs decorator)
156156+}
157157+```
158158+159159+**Maps to:**
160160+```json
161161+{
162162+ "type": "object",
163163+ "required": ["createdAt"],
164164+ "properties": {
165165+ "text": {"type": "string"},
166166+ "createdAt": {"type": "string", "format": "datetime"}
167167+ }
168168+}
169169+```
170170+171171+## Primitive Types
172172+173173+| TypeSpec | Lexicon JSON |
174174+|----------|--------------|
175175+| `boolean` | `{"type": "boolean"}` |
176176+| `integer` | `{"type": "integer"}` |
177177+| `string` | `{"type": "string"}` |
178178+| `bytes` | `{"type": "bytes"}` |
179179+| `cidLink` | `{"type": "cid-link"}` |
180180+| `unknown` | `{"type": "unknown"}` |
181181+182182+## Format Types
183183+184184+Specialized string formats:
185185+186186+| TypeSpec | Lexicon Format |
187187+|----------|----------------|
188188+| `atIdentifier` | `at-identifier` - Handle or DID |
189189+| `atUri` | `at-uri` - AT Protocol URI |
190190+| `cid` | `cid` - Content ID |
191191+| `datetime` | `datetime` - ISO 8601 datetime |
192192+| `did` | `did` - DID identifier |
193193+| `handle` | `handle` - Handle identifier |
194194+| `nsid` | `nsid` - Namespaced ID |
195195+| `tid` | `tid` - Timestamp ID |
196196+| `recordKey` | `record-key` - Record key |
197197+| `uri` | `uri` - Generic URI |
198198+| `language` | `language` - Language tag |
199199+200200+## Unions
201201+202202+### Open Unions (Common Pattern)
203203+204204+**Open unions are the default and preferred in lexicons.** Add `unknown` to mark as open:
205205+206206+```typescript
207207+model Main {
208208+ /** Can be any of these types or future additions */
209209+ @required item: TypeA | TypeB | TypeC | unknown;
210210+}
211211+212212+model TypeA {
213213+ @readOnly @required kind: string = "a";
214214+ @required valueA: string;
215215+}
216216+```
217217+218218+**Maps to:**
219219+```json
220220+{
221221+ "properties": {
222222+ "item": {
223223+ "type": "union",
224224+ "refs": ["#typeA", "#typeB", "#typeC"]
225225+ }
226226+ }
227227+}
228228+```
229229+230230+The `unknown` makes it open but doesn't appear in refs.
231231+232232+### Known Values (Open String Enum)
233233+234234+Suggest values but allow others:
235235+236236+```typescript
237237+model Main {
238238+ /** Language - suggests common values but allows any */
239239+ lang?: "en" | "es" | "fr" | string;
240240+}
241241+```
242242+243243+**Maps to:**
244244+```json
245245+{
246246+ "properties": {
247247+ "lang": {
248248+ "type": "string",
249249+ "knownValues": ["en", "es", "fr"]
250250+ }
251251+ }
252252+}
253253+```
254254+255255+### Closed Unions (Discouraged)
256256+257257+**⚠️ Closed unions are discouraged in lexicons** as they prevent future additions. Use only when absolutely necessary:
258258+259259+```typescript
260260+@closed
261261+@inline
262262+union Action {
263263+ Create,
264264+ Update,
265265+ Delete,
266266+}
267267+268268+model Main {
269269+ @required action: Action;
270270+}
271271+```
272272+273273+**Maps to:**
274274+```json
275275+{
276276+ "properties": {
277277+ "action": {
278278+ "type": "union",
279279+ "refs": ["#create", "#update", "#delete"],
280280+ "closed": true
281281+ }
282282+ }
283283+}
284284+```
285285+286286+### Closed Enums (Discouraged)
287287+288288+**⚠️ Closed enums are also discouraged.** Use `@closed @inline union` for fixed value sets:
289289+290290+```typescript
291291+@closed
292292+@inline
293293+union Status {
294294+ "active",
295295+ "inactive",
296296+ "pending",
297297+}
298298+```
299299+300300+**Maps to:**
301301+```json
302302+{
303303+ "type": "string",
304304+ "enum": ["active", "inactive", "pending"]
305305+}
306306+```
307307+308308+Integer enums work the same way:
309309+310310+```typescript
311311+@closed
312312+@inline
313313+union Fibonacci {
314314+ 1, 2, 3, 5, 8,
315315+}
316316+```
317317+318318+## Arrays
319319+320320+Use `[]` suffix:
321321+322322+```typescript
323323+model Main {
324324+ /** Array of strings */
325325+ stringArray?: string[];
326326+327327+ /** Array with size constraints */
328328+ @minItems(1)
329329+ @maxItems(10)
330330+ limitedArray?: integer[];
331331+332332+ /** Array of references */
333333+ items?: Item[];
334334+335335+ /** Array of union types */
336336+ mixed?: (TypeA | TypeB | unknown)[];
337337+}
338338+```
339339+340340+**Maps to:** `{"type": "array", "items": {...}}`
341341+342342+**Note:** `@minItems`/`@maxItems` in TypeSpec map to `minLength`/`maxLength` in JSON.
343343+344344+## Blobs
345345+346346+```typescript
347347+model Main {
348348+ /** Basic blob */
349349+ file?: Blob;
350350+351351+ /** Image up to 5MB */
352352+ image?: Blob<#["image/*"], 5000000>;
353353+354354+ /** Specific types up to 2MB */
355355+ photo?: Blob<#["image/png", "image/jpeg"], 2000000>;
356356+}
357357+```
358358+359359+**Maps to:**
360360+```json
361361+{
362362+ "file": {"type": "blob"},
363363+ "image": {
364364+ "type": "blob",
365365+ "accept": ["image/*"],
366366+ "maxSize": 5000000
367367+ }
368368+}
369369+```
370370+371371+## References
372372+373373+### Local References
374374+375375+Same namespace, uses `#`:
376376+377377+```typescript
378378+model Main {
379379+ metadata?: Metadata;
380380+}
381381+382382+model Metadata {
383383+ @required key: string;
384384+}
385385+```
386386+387387+**Maps to:** `{"type": "ref", "ref": "#metadata"}`
388388+389389+### External References
390390+391391+Different namespace to specific def:
392392+393393+```typescript
394394+model Main {
395395+ externalRef?: com.example.defs.Metadata;
396396+}
397397+```
398398+399399+**Maps to:** `{"type": "ref", "ref": "com.example.defs#metadata"}`
400400+401401+Different namespace to main def (no fragment):
402402+403403+```typescript
404404+model Main {
405405+ mainRef?: com.example.post.Main;
406406+}
407407+```
408408+409409+**Maps to:** `{"type": "ref", "ref": "com.example.post"}`
410410+411411+## Tokens
412412+413413+Empty models marked with `@token`:
414414+415415+```typescript
416416+/** Indicates spam content */
417417+@token
418418+model ReasonSpam {}
419419+420420+/** Indicates policy violation */
421421+@token
422422+model ReasonViolation {}
423423+424424+model Report {
425425+ @required reason: (ReasonSpam | ReasonViolation | unknown);
426426+}
427427+```
428428+429429+**Maps to:**
430430+```json
431431+{
432432+ "report": {
433433+ "properties": {
434434+ "reason": {
435435+ "type": "union",
436436+ "refs": ["#reasonSpam", "#reasonViolation"]
437437+ }
438438+ }
439439+ },
440440+ "reasonSpam": {
441441+ "type": "token",
442442+ "description": "Indicates spam content"
443443+ }
444444+}
445445+```
446446+447447+## Operation Details
448448+449449+### Query Parameters
450450+451451+```typescript
452452+@query
453453+op main(
454454+ @required search: string,
455455+ limit?: integer = 50,
456456+ tags?: string[]
457457+): { ... };
458458+```
459459+460460+Parameters can be inline with decorators before each.
461461+462462+### Procedure with Input and Parameters
463463+464464+```typescript
465465+@procedure
466466+op main(
467467+ input: {
468468+ @required data: string;
469469+ },
470470+ parameters: {
471471+ @required repo: atIdentifier;
472472+ validate?: boolean = true;
473473+ }
474474+): { ... };
475475+```
476476+477477+Use `input:` for body, `parameters:` for query params.
478478+479479+### No Output
480480+481481+```typescript
482482+@procedure
483483+op main(input: {
484484+ @required uri: atUri;
485485+}): void;
486486+```
487487+488488+Use `: void` for procedures with no output.
489489+490490+### Output Without Schema
491491+492492+```typescript
493493+@query
494494+@encoding("application/json")
495495+op main(id?: string): never;
496496+```
497497+498498+Use `: never` with `@encoding()` for output with encoding but no schema.
499499+500500+### Errors
501501+502502+```typescript
503503+/** The provided text is invalid */
504504+model InvalidText {}
505505+506506+/** User not found */
507507+model NotFound {}
508508+509509+@procedure
510510+@errors(InvalidText, NotFound)
511511+op main(...): ...;
512512+```
513513+514514+Empty models with descriptions become error definitions.
515515+516516+## Constraints
517517+518518+### String Constraints
519519+520520+```typescript
521521+model Main {
522522+ /** Byte length constraints */
523523+ @minLength(1)
524524+ @maxLength(100)
525525+ text?: string;
526526+527527+ /** Grapheme cluster length constraints */
528528+ @minGraphemes(1)
529529+ @maxGraphemes(50)
530530+ displayName?: string;
531531+}
532532+```
533533+534534+**Maps to:** `minLength`/`maxLength`, `minGraphemes`/`maxGraphemes`
535535+536536+### Integer Constraints
537537+538538+```typescript
539539+model Main {
540540+ @minValue(1)
541541+ @maxValue(100)
542542+ score?: integer;
543543+}
544544+```
545545+546546+**Maps to:** `minimum`/`maximum`
547547+548548+### Bytes Constraints
549549+550550+```typescript
551551+model Main {
552552+ @minBytes(1)
553553+ @maxBytes(1024)
554554+ data?: bytes;
555555+}
556556+```
557557+558558+**Maps to:** `minLength`/`maxLength`
559559+560560+**Note:** Use `@minBytes`/`@maxBytes` in TypeSpec, but they map to `minLength`/`maxLength` in JSON.
561561+562562+### Array Constraints
563563+564564+```typescript
565565+model Main {
566566+ @minItems(1)
567567+ @maxItems(10)
568568+ items?: string[];
569569+}
570570+```
571571+572572+**Maps to:** `minLength`/`maxLength`
573573+574574+**Note:** Use `@minItems`/`@maxItems` in TypeSpec, but they map to `minLength`/`maxLength` in JSON.
575575+576576+## Default and Constant Values
577577+578578+### Defaults
579579+580580+```typescript
581581+model Main {
582582+ version?: integer = 1;
583583+ lang?: string = "en";
584584+}
585585+```
586586+587587+**Maps to:** `{"default": 1}`, `{"default": "en"}`
588588+589589+### Constants
590590+591591+Use `@readOnly` with default value:
592592+593593+```typescript
594594+model Main {
595595+ @readOnly status?: string = "active";
596596+}
597597+```
598598+599599+**Maps to:** `{"const": "active"}`
600600+601601+## Nullable Fields
602602+603603+Use `| null` for nullable fields:
604604+605605+```typescript
606606+model Main {
607607+ @required createdAt: datetime;
608608+ updatedAt?: datetime | null; // can be omitted or null
609609+ deletedAt?: datetime; // can only be omitted
610610+}
611611+```
612612+613613+**Maps to:**
614614+```json
615615+{
616616+ "required": ["createdAt"],
617617+ "nullable": ["updatedAt"],
618618+ "properties": { ... }
619619+}
620620+```
621621+622622+## Common Patterns
623623+624624+### Discriminated Unions
625625+626626+Use `@readOnly` with const for discriminator:
627627+628628+```typescript
629629+model Create {
630630+ @readOnly @required type: string = "create";
631631+ @required data: string;
632632+}
633633+634634+model Update {
635635+ @readOnly @required type: string = "update";
636636+ @required id: string;
637637+}
638638+```
639639+640640+### Nested Unions
641641+642642+```typescript
643643+model Container {
644644+ @required id: string;
645645+ @required payload: (PayloadA | PayloadB | unknown);
646646+}
647647+```
648648+649649+Unions can be nested in objects and arrays.
650650+651651+## Naming Conventions
652652+653653+Model names convert from PascalCase to camelCase in defs:
654654+655655+```typescript
656656+model StatusEnum { ... } // becomes "statusEnum"
657657+model UserMetadata { ... } // becomes "userMetadata"
658658+model Main { ... } // becomes "main"
659659+```
660660+661661+## Decorator Style
662662+663663+- Single `@required` goes on same line: `@required text: string`
664664+- Multiple decorators go on separate lines with blank line after:
665665+ ```typescript
666666+ @minLength(1)
667667+ @maxLength(100)
668668+ text?: string;
669669+ ```
-115
LEXICON.md
···11-More folks are starting to design Lexicon schemas from scratch, which is great! This is particularly driven by ecosystem projects like Slices and microcosm which make it easier to work with AT network data in a generic way.
22-33-One piece of developer documentation that has been missing is guidance on writing Lexicons themselves: a Lexicon style guide, or Lexinomincon. Below is an early draft of such a guide, which we intend to integrate in to the the atproto.com website. We are also working on linting tools and other resources for developers drafting and publishing lexicon schemas.
44-55-This will probably be a living document, but early questions and feedback are very much welcome.
66-77-Basic Guidelines
88-Name casing conventions:
99-1010-Schemas & attributes: Use lowerCamelCase capitalization for schemas and names (as opposed to UpperCamelCase, snake_case, ALL_CAPS, etc).
1111-API error names: UpperCamelCase
1212-Fixed strings (eg knownValues): kebab-case
1313-Acceptable characters:
1414-1515-Field names should stick to the same character set as schema names (NSID name segments): ASCII alphanumeric, first character not a digit, no hyphens, case-sensitive
1616-Exceptions may be justifiable in some situations, such as preservation of names in existing external schemas
1717-Data objects should never contain schema-specified field names starting with $ at any level of nesting; these are reserved for future protocol-level extensions
1818-Naming conventions:
1919-2020-Use singular nouns for record schemas
2121-eg post, like, profile
2222-Use “verb-noun” for query and procedure endpoints
2323-eg getPost, listLikes, putProfile
2424-Common verbs for query endpoints are: get, list, search (for full-text search), query (for flexible matching or filtering filtering)
2525-Common verbs for procedure endpoints: create, update, delete, upsert, put
2626-Use “subscribe-plural-noun” for subscription
2727-eg subscribeLabels
2828-Conventions for permission-set schema naming has not be established yet, but probably has “auth” prefix (eg, authBasic)
2929-If an endpoint is experimental, unstable, or not intended for interoperability, indicate that in the NSID name
3030-eg, include .temp. or .unspecced. in the NSID hierarchy
3131-Avoid generic names which conflict with popular programming language conventions
3232-eg, avoid using default or length as schema names
3333-Documentation and Completeness:
3434-3535-Add a description to every main schema definition (records, API endpoints, etc)
3636-for API endpoints, mention in the description if authentication is required, and whether responses will be personalized if authentication is optional
3737-Add descriptions to potentially ambiguous fields and properties. This is particularly important for fields with generic names like uri or cid: CID of what?
3838-NSID namespace grouping:
3939-4040-Many applications and projects will have multiple distinct functions or features, and schemas of all types can have that grouping represented in the NSID hierarchy
4141-eg app.bsky.feed.* , app.bsky.graph.*
4242-Very simple applications can include all endpoints under a single NSID “group”
4343-use a .defs schema for definitions which might be reused by multiple schemas in the same namespace, or by third parties
4444-eg app.bsky.feed.defs
4545-putting these in a separate schema file means that deprecation or removal of other schema files doesn’t impact reuse
4646-Avoid conflicts and confusion between groups, names, and definitions
4747-eg app.bsky.feed.post#main vs app.bsky.feed.post.main, or com.example.record#foo and com.example.record.foo
4848-or defining both app.bsky.feed (as a record) and app.bsky.feed.post (with app.bsky.feed as a group)
4949-Other guidelines:
5050-5151-Specify the format of string fields when appropriate
5252-String fields in records should almost always have a maximum length if they don’t have a format type
5353-Don’t redundantly specify both a format and length limits
5454-If limiting the length of a string for semantic or visual reasons, grapheme limits should be used to ensure a degree of consistency across human languages. A data size (bytes) limit should also be added in these cases. A ratio of between 10 to 20 bytes to 1 grapheme is recommended.
5555-The string and bytes record data types are intended for constrained data size use-cases. For text or binary data of larger size, blob references should be used. This can include longer-form text and structured data.
5656-Enum sets are “closed” and can not be updated or extended without breaking schema evolution rules. For this reason they should almost always be avoided.
5757-For strings, knownValues provides more flexible alternative
5858-String knownValues may include simple string constants, or may include schema references to a token (eg, the string "com.example.defs#tokenOne")
5959-Tokens provide an extension mechanism, and work well for values that have subjective definitions or may be expanded over time
6060-See com.atproto.moderation.defs#reasonType and com.atproto.sync.defs#hostStatus for two contrasting instances, the former extensible and the later more constrained
6161-Take advantage of re-usable definitions, such as com.atproto.repo.strongRef (for versioned references to records) or com.atproto.label.defs#label (in an array, for hydrated labels)
6262-API endpoints which take an account identifier as an argument (eg, query parameter) should use at-identifier so that clients can avoid calling resolveHandle if they only have an account handle
6363-Record schemas should always use persistent identifiers (DIDs) for references to other accounts, instead of handles
6464-API endpoints should always specify an output with encoding, even if they have no meaningful response data
6565-a good default is application/json with the schema being an object with no defined properties
6666-Optional boolean fields should be phrased such that false is the default and expected value
6767-For example, if an endpoint can return a mix of “foo” and “bar”, and the common behavior is to include “foo” but not “bar”, then controlling parameters should be named excludeFoo (default false) and includeBar (default false), as opposed to excludeBar (default true)
6868-Content hashes (CIDs) may be represented as a string format or in binary encoding (cid-link)
6969-In most situations, including versioned references between records, the string format is recommended.
7070-Binary encoding is mostly used for protocol-level mechanisms, such as the firehose.
7171-Schema Evolution and Extension
7272-All schemas should be flexible to extension and evolution over time, without breaking the Lexicon schema evolution rules. This is particularly true for record schemas. Given the distributed storage model of atproto, developers do not have a reliable mechanism to update all data records in the network. Extensions could come from the original designer, or other developers and projects.
7373-7474-Experimental schemas and projects can use variant NSIDs (eg, including .temp. in the name hierarchy) to develop in the live network without committing to a stable record data schemas.
7575-7676-Major non-backwards-compatible schema changes are possible by declaring a new schema. The current naming convention is to append “V2” to the original name (or “V3”, etc).
7777-7878-Design recommendations to make schemas flexible to future evolution and extension:
7979-8080-do not mark data fields or API parameters as required unless they are truly required for functionality
8181-required fields can not be made optional or deprecated under the evolution rules
8282-you can add new optional fields to a schema without changing backwards compatibility or requiring a V2 schema, but you can’t add new required fields
8383-use object types containing a single element/field instead of atomic data types in arrays, to allow additional context to be included in the future
8484-for example, in an API response listing accounts (DIDs), return an array of objects each with an account field listing the DID, instead of an array of strings
8585-make unions “open” in almost all situations, to allow future addition of types or values
8686-open unions can be an extension mechanism for third parties to include self-defined data types
8787-Design Patterns
8888-There is a basic convention for pagination of query API endpoints:
8989-query parameters include an optional limit (integer) and optionalcursor (string)
9090-the output body includes optional cursor (string) and a required array of response objects (with context-specific pluralized field name)
9191-the initial client request does not define a cursor. If the response includes a cursor, then more results are available, and the client should query again with the new cursor to get more results
9292-the limit value is an upper limit, and the response may include fewer (or even zero) results, while further results are still available. It is the lack of cursor in responses that indicates pagination is complete. The response set may have items removed if they are tombstoned or have been otherwise filtered from the response set.
9393-There is also a convention for subscription endpoints which support “sequencing” and backfill cursors:
9494-the endpoint has an optional cursor query parameter (integer)
9595-all core message types include a seq field (integer). The seq of messages increases monotonically, though may have gaps.
9696-if the cursor is not provided, the server will start returning new messages from the current point forward
9797-if the cursor is provided, the server will attempt to return historical messages starting with the matching seq, continuing through to the current stream
9898-if the cursor is in the future (higher than the current sequence), an error is returned and the connection closed (TODO: is this the convention?)
9999-if the cursor is older than the earliest available message (or is 0), the server returns an info message of name OutdatedCursor, then returns messages starting from the oldest available
100100-A common pattern in API responses is to include “hydrated views” of data records. For example, when viewing an account’s profile, the response might include CDN or thumbnail URLs for any media files, moderation labels, global aggregations, and viewer-specific social graph context.
101101-For detailed views, a best practice to include the original record verbatim, instead of defining a new schema with a superset of fields. This is easier to maintain (can’t forget to update fields), and ensures any off-schema extension data is included.
102102-Viewer-specific metadata should be optional and either indicated in descriptions or grouped under a sub-object. This makes schemas reusable between “public” and “logged-in” views, and makes it clearer what information will be available when.
103103-A helpful pattern for application developers is to ensure there is an API endpoint that accepts a reference to a record (eg, a AT URI or equivalent; or multiple references) returns the hydrated data object(s).
104104-the app.bsky.richtext.facet system can be used to annotate short text strings in a way that is simpler and safer to work with than full-featured markup languages
105105-for more details see "Why RichText facets in Bluesky"
106106-the feature type system is an open union which can be extended with additional types
107107-more powerful systems like Markdown are more appropriate for long-form text
108108-One pattern for extending or supplementing a record is to define “sidecar” records in the same account repository with the same record key and different types (collections).
109109-Sidecar records can be defined and managed by the original Lexicon designer or by independent developers.
110110-The sidecar records can be updated (mutated) without breaking strong references to the original record.
111111-Sidecar context can be included in API responses.
112112-Because atproto accounts can be used flexibly with any application in the network, it can be ambiguous which accounts are participating in a particular app modality. This can be clarified if there is a known representative record type for the modality, and that clients create such a record for active accounts. Deletion of this record can be a way to indicate the user is no longer active. This works best if the record has a single known instance (fixed record key).
113113-For example, an-app specific “profile” or “declaration” record can indicate that the account has logged in to an associated app at least once, even if the record is “empty”.
114114-Backfill services can enumerate all accounts in the network with the given signaling record, and also process deletion of that record as deactivation of that modality.
115115-This design pattern is strongly recommended for new app modalities.