atproto made simple: publishing lexicons

i find some atproto documentation extremely confusing. the section about publishing lexicons is one of those parts that makes me want to tear my hair out. it sounds so hopelessly complicated.

so i'm here to tell you publishing lexicons is actually very simple.

yes, you can use the goat tool to do it for you , but i want to give you a clear mental model so you know what it does and why.

what are lexicons anyway

i'll assume you're familiar with lexicons but let's recap.

lexicons are type definitions (written in JSON) which let atproto apps understand data from each other. you've probably seen these:

{
"lexicon": 1,
"id": "app.bsky.actor.profile",
"defs": {
"main": {
"type": "record",
"key": "self",
"record": {
"type": "object",
"properties": {
"displayName": { "type": "string", "maxGraphemes": 64 },
"description": { "type": "string", "maxGraphemes": 256 }
}
}
}
}
}

if you squint at it, you could see it's trying to say something like

namespace app.bsky.actor {
@record('self')
type profile {
@maxGraphemes(64) displayName: string
@maxGraphemes(256) description: string
}
}

(this is made-up syntax.) so -- it's just a type definition.

lexicons are mostly used for typing atproto records (e.g. "a Bluesky profile" is a record following app.bsky.actor.profile lexicon, "a Leaflet publication" follows pub.leaflet.publication lexicon, and so on). if you're making an atproto app that stores its own data, you'll probably want to define a lexicon per data type you store.

they're also used for typing atproto-flavored API calls, e.g. a PDS implements com.atproto.repo.getRecord lexicon, a Bluesky appview implements app.bsky.feed.searchPosts lexicon, and so on. i've never wanted to define one but sometimes i need to call those.

okay, so lexicons are just type definitions in a JSON dialect, and they can be used to define the shape of records or of API calls.

so how do you "publish" one, and why would you want that?

lexicons are just records

the point of type definitions is to let apps understand each other, so there needs to be some canonical place where people can find them.

for example, if you want to know the app.bsky.actor.profile definition, where do you go looking for it? where should tooling look for it? ideally there should be a canonical place where it lives.

in atproto, this is done in a wonderfully simple way -- a lexicon itself is just a regular record in the app developer's repository.

recall that records in atproto look like this:

at://did/collection/rkey

for example:

at://did:bla/app.bsky.actor.profile/self

at://did:bla/app.bsky.feed.post/3mjffqvyo4c2f

at://did:bla/dev.npmx.feed.like/3me7fhoewyp2k

the right part ("record key") identifies the record among its siblings. it's often a timestamp but is conventionally a constant self for profiles (since you only need one profile). for lexicons, however, it'll be the lexicon name itself. lexicons themselves are records in a special com.atproto.lexicon.schema collection:

at://did:bla/com.atproto.lexicon.schema/app.bsky.actor.profile

at://did:bla/com.atproto.lexicon.schema/app.bsky.feed.post

at://did:bla/com.atproto.lexicon.schema/dev.npmx.feed.like

so to publish a lexicon, you need to create a record like this.

publishing a lexicon

let's walk through actually publishing some lexicon.

step 1: create a record

first, choose account (did) where you want to put your lexicons. you could choose any account, it doesn't matter what its handle is.

you can start with your personal one and maybe change it later.

to publish a lexicon, put your lexicon file into a collection called com.atproto.lexicon.schema, with lexicon name as record key.

for example, to publish app.example.post lexicon, put it into com.atproto.lexicon.schema collection of some account you own:

at://did:bla/com.atproto.lexicon.schema/app.example.post

you can do this directly from pdsls via "create record" flow. the record is your lexicon JSON. don't forget "lexicon": 1 in it.

// at://did:bla/com.atproto.lexicon.schema/app.example.post
{
"lexicon": 1,
"id": "app.example.post",
"defs": {
// ... your lexicon, as usual ...
}
}

step 2: prove you own the domain

but wait!

anyone can publish records. how does anyone know that your lexicon is the "true" version, i.e. that you actually control app.example.*?

the answer is very similar to how you connect domain to atproto handles -- you do this with a DNS record. for example, to connect @example.app to did:foo, you had to add a DNS record like:

_atproto.example.app. TXT "did=did:foo"

the mechanism is similar, but instead of _atproto you're gonna need _lexicon. to say that did:foo owns app.example.* lexicons, put:

_lexicon.example.app. TXT "did=did:foo"

into the example.app DNS settings.

that's all! now did:foo is the "lexicon authority" for app.example.*

note: this doesn't work transitively for subdomains. to also show you "control" a nested namespace like app.example.xxx.*, you'd have to create a DNS TXT record for _lexicon.xxx.example.app.

verifying it worked

once you've done these two steps, your lexicons are hooked up.

you can just write a lexicon name into https://pdsls.dev/ and it'll resolve it via DNS and show your lexicon if it is published.

for example take app.bsky.actor.profile.

it consists of the "namespace" part (app.bsky.actor) and the actual lexicon name (profile). currently pdsls resolves it to at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.actor.profile . but how does pdsls find that did?

it checks DNS for _lexicon + reverse namespace (actor.bsky.app):

dig TXT _lexicon.actor.bsky.app

that DNS record currently says this lexicon is owned by "did=did:plc:4v4y5r3lwsbtmsxhile2ljac"

what's that account by the way? apparently it's @bsky-lexicons.bsky.social . it even has a fancy profile picture.

you can browse all lexicons published by it here: https://pdsls.dev/at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema . com.atproto.lexicon.schema is just a collection like any other so you can browse it on pdsls just like Bluesky posts.

if you published your lexicon, you should be able to do the same with your lexicon as well, i.e. pdsls should resolve app.example.post.

publishing lexicons is awesome because they start showing up in pdsls and all the other tooling. you should publish your lexicons.

updating a lexicon

to update a lexicon, you just update that record. keep in mind that lexicons in active use should be evolved gracefully . in short, you can add new optional fields and open union cases, but you should neither tighten nor relax existing constraints since that breaks applications.

if your app has zero users, don't overthink it -- just update it.

that's it?

yeah.

you'll probably want to validate lexicons before you publish them which goat can help you with. check this and this guide too.

hope this helps!

optional: homework

find the account that owns the com.atproto.lexicon.schema lexicon. hint: you might find dig helpful again. (yes, this is delightfully meta, though also kind of useless as you'll see.)