clause 1
the schema of a schema is deliberately dumb
Every lexicon file shares one shape: a top-level
{ "lexicon": 1, "id": "<nsid>", "defs": {...} }.
defs is a map of named type definitions — by
convention the one that matters is called main —
each tagged with a type: record,
object, string, token,
union, array, blob,
ref, and a handful more for the RPC side
(query, procedure,
subscription) that a pure data record like a post
never uses. A record def adds one more thing: a
key strategy — almost always tid, a
time-sortable ID the client mints, though a few lexicons use a
literal constant key (for singletons like a profile) or let the
client pick any string.
defs.main.record.properties block, in one file,
owned by whoever's NSID it's published under. This is
choice 1 from the companion
page made concrete: the protocol didn't build a social
ontology and expose it — it built a way to publish one, and
app.bsky.* is the first tenant, not a built-in.
clause 2
the thinnest possible edge: reading app.bsky.graph.follow
{
"lexicon": 1,
"id": "app.bsky.graph.follow",
"defs": {
"main": {
"type": "record",
"description": "...Duplicate follows will be ignored by the AppView.",
"key": "tid",
"record": {
"type": "object",
"required": ["subject", "createdAt"],
"properties": {
"subject": { "type": "string", "format": "did" },
"createdAt": { "type": "string", "format": "datetime" },
"via": { "type": "ref", "ref": "com.atproto.repo.strongRef" }
}
}
}
}
}
That's the entire record. A DID to point at, a timestamp, and an
optional provenance pointer (via — added later, a
strongRef to whatever surfaced the account, e.g. a
feed or another post, so a client can show "followed via ...").
No relationship ID, no status field, no acceptance step. The
description line is doing real work too: "Duplicate follows
will be ignored by the AppView" is an admission that the
schema itself enforces no uniqueness at all — a repo can contain
the same follow record ten times over, each one individually
valid, and the lexicon has no vocabulary for "don't." Dedup is
entirely a convention some AppView chooses to apply at read
time, not a constraint the type system can express.
clause 3
one addressing primitive, reused for everything that points at anything
The companion page already
covers what a strongRef means for a like — URI plus
CID, a pointer to one exact, byte-level revision rather than a
live, editable thing. What's worth adding once you've read the
files: it is the only cross-reference type in the
entire social graph. A reply's root and
parent, a like's subject, a repost's
target, a quote-embed's record, and now a follow's
via — every single one of these resolves to the
same four-line, two-field { uri, cid } object
defined once in com.atproto.repo.strongRef and
ref'd in from everywhere else. There is no second,
looser "just point at a URI, don't pin the bytes" reference type
anywhere in the core social lexicons — if you want to point at
another record at all, you pin it.
clause 4
open categories, soft enums
app.bsky.graph.list's purpose field
isn't a plain string or an inline closed list — it's a
ref to a named token def
(app.bsky.graph.defs#listPurpose), which is how the
lexicon language expresses "a fixed vocabulary of named things,"
similar to an enum. But the more common way a lexicon
constrains a string is softer than that: com.atproto.label.defs'
val field — the actual text of a moderation
label, "porn," "spam," "bot" — is typed as a plain
string with a knownValues list
attached, and knownValues is documentation, not
enforcement. A labeler can publish a label with
val: "anything-it-invents" and the record still
validates; nothing about the type refuses it.
token/enum when a fixed vocabulary is really
wanted, an open string with knownValues as a hint
when it isn't — and the ecosystem's actual moderation vocabulary
picked the open one. Anyone running a labeler can mint a new
category of judgment the day they need it, with no lexicon
change and no one's permission, the same "namespace as ontology"
freedom from choice 1 applied one level down, to values instead
of whole record types. The cost is the same cost too: a client
that's never heard of your invented label value has to decide,
unguided, what to do with a fact it doesn't recognize.
clause 5
additive only, forever — schemas can grow but never shrink
The lexicon spec's compatibility rules are short and absolute:
new fields must be optional; a required field can never become
optional or be removed; a field's name and type are permanent
once published; and a change that would break any of that isn't
a version bump — it's a brand new NSID. follow
picking up via as an optional field is what
compliant evolution actually looks like in the wild; there is no
path in this system that lets app.bsky.feed.post
rename text or make createdAt
optional-where-it-used-to-be-required.
clause 6
what the typed graph can't hold
Put the fields side by side and a pattern falls out: everything this type system can represent is a fact one identity can assert unilaterally, about its own record, without anyone else's cooperation or a central party enforcing consistency. Just as clearly, several ordinary social facts have no type anywhere in these lexicons — not "haven't gotten around to it," structurally absent:
| can represent | can't represent |
|---|---|
| a one-directional claim, signed by exactly one DID | a mutual relationship as a single, jointly-committed fact — "friends" is always two unilateral records an AppView chose to join |
a label's retraction (neg: true is a real field on com.atproto.label.defs#label) |
a follow's or like's retraction-with-history — deleting the record just erases it; there's no typed "I used to, then stopped" object for ordinary social edges the way there is for labels |
| presence of an edge (you follow, or you don't) | weight, strength, or qualification on that edge — a "close friend" isn't a parameter on follow, it's a wholly separate record type (a list) bolted on beside it |
a byte-exact pointer to another record via strongRef |
any guarantee that pointer still resolves to anything — a like's subject can point at a since-deleted post forever; the type validates shape, never reachability |
| an individual write, atomically committed to your own repo | a transaction spanning two repos, or even two collections in one repo — "this reply implies that thread's reply-count changes" has no typed unit that commits both sides together |
| a public, resolvable record at a stable AT-URI | confidentiality as a schema-level concept — none of the core social lexicons have a private: true the type system enforces; visibility is a PDS/service access-control question, not an ontological one here |
neg) and ordinary social edges don't. Moderation
was considered important enough to need a typed "undo," and
follows and likes weren't. That's a real, specific, load-bearing
decision about whose history gets preserved by the schema and
whose doesn't, not an inevitability of the type system.