bottlecaps.beer #
Beer tracking on atproto — log the beers you drink, where you drank them, and what you thought.
Lexicons #
Records live under the beer.bottlecaps.* namespace (NSID authority: bottlecaps.beer).
beer.bottlecaps.brewery #
A declaration that an account is a brewery, published at the fixed key self
(one brewery per account, like app.bsky.actor.profile). Carries name
(required), description, url, a logo blob, locations (an array of the
community location lexicons — the brewery, taprooms, brewpubs), and createdAt.
Publishing this record makes the account's repo the authoritative home for its beers. Whether the account really is the brewery it claims to be is a separate question, answered by verification (below).
beer.bottlecaps.beer #
A beer in the catalog: name (required), style, abv, ibu, description,
label blob, createdAt.
Attribution comes from repo ownership. A beer published in a brewery's repo is that brewery's beer — no one can write into another brewery's repo, so no one can forge official beers. Two supporting fields:
breweryName— free-text brewery name, only meaningful for community-submitted beers whose brewery isn't on atproto. Clients display these as unverified catalog entries, and ignore the field entirely when the publishing repo has a brewery record.collaborators— DIDs of other breweries on a collab brew. A claim alone proves nothing (brewery A could list brewery B without consent), which is why collabs are a two-sided handshake (below).
beer.bottlecaps.sameAs #
Cross-links duplicate beer records so the catalog stays open (anyone can
publish a beer) while still converging on one official version. The record
asserts subject (a duplicate or community-submitted beer) is the same beer
as target (the preferred version, typically the one in the brewery's own
repo).
A link is authoritative when its publisher owns either end:
- a brewery links a community entry → its own beer ("that's our beer, here's the official record")
- a community submitter links their entry → the official one ("mine is a duplicate of that")
Links between two records the publisher owns neither of are suggestions the AppView can queue for human review. The AppView unions authoritative links (following chains transitively) into clusters, picks a canonical member by tier — verified brewery repo, else curator repo, else oldest community entry — and aggregates check-ins and ratings across the whole cluster. If two verified breweries claim the same community entry, that's a visible conflict for moderation, not something the schema resolves.
beer.bottlecaps.collab #
The other half of the collab handshake. The collaborating brewery publishes
this in its own repo with a subject strong ref pointing at the beer. An
AppView only treats a collaborator as confirmed when the beer's collaborators
entry and a matching collab record from that DID both exist. Either side can
delete its record to dissolve the claim.
app.bsky.graph.verification (Bluesky's, reused as-is) #
We use Bluesky's actual verification lexicon rather than defining our own. A
trusted issuer (the bottlecaps.beer service account, to start) publishes a
verification record whose subject is the brewery account's DID, with the
handle and displayName captured at verification time — if either changes
afterward, the verification self-invalidates. displayName should be the
brewery's name.
Anyone can publish verification records, so they carry no authority by themselves; apps decide which issuer DIDs to trust.
Catalog curation flow #
The catalog has three tiers, stitched together by sameAs chains:
- Community submission. A user defines a beer in their own repo
(
breweryNamefilled in) and can check into it immediately. - Verified canonical. The bottlecaps.beer service reviews the submission,
publishes a cleaned-up canonical record in its own repo, and links the
community entry to it with
sameAs(authoritative: we own the target). The AppView treats the service repo as a trusted catalog curator, so these display as verified entries even though no brewery is on atproto yet. - Brewery official. When a brewery joins and gets verified, it publishes
its own beer records, and the service links each canonical to the brewery's
record with
sameAs(authoritative: we own the subject). The chain community → canonical → official resolves transitively; the brewery's copy becomes the canonical face.
Records are never deleted during promotion — check-ins strong-ref whatever record the user originally found, and deleting a superseded record would strand them. Old tiers stay in place as permanent redirects.
Trust model, end to end #
- An account publishes
beer.bottlecaps.breweryat keyself→ it claims to be a brewery. - A trusted verifier publishes
app.bsky.graph.verificationfor that DID → the claim is believed, the account gets an "official" badge. - Beers in that repo are that brewery's beers — enforced by repo ownership, not by policy.
- Collab beers list collaborator DIDs, and each collaborator confirms with a
beer.bottlecaps.collabrecord in its own repo. - Beers from repos without a brewery record are unverified community
submissions (
breweryNamefills in the display name).
Check-ins #
beer.bottlecaps.checkin #
A log of drinking a beer. Fields:
beer(required) — strong ref to abeer.bottlecaps.beerrecordrating— half-star increments, 1–10 (10 = 5 stars)notes— tasting notesserving—draft,bottle,can,cask,growler,crowler,tasterlocation— union of the community location lexicons:community.lexicon.location.address/fsq/geo/hthree(same pattern ascommunity.lexicon.calendar.event)photo— an image blobcreatedAt(required)
Vendored lexicons #
The lexicons/community/ and lexicons/app/ files are verbatim copies of the
community location lexicons and Bluesky's verification lexicon, vendored so
code generation can resolve refs. They are not ours — upstream is the source
of truth.