Bottle Caps Social Drinking Site

Files
README.md

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:

  1. Community submission. A user defines a beer in their own repo (breweryName filled in) and can check into it immediately.
  2. 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.
  3. 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 #

  1. An account publishes beer.bottlecaps.brewery at key self → it claims to be a brewery.
  2. A trusted verifier publishes app.bsky.graph.verification for that DID → the claim is believed, the account gets an "official" badge.
  3. Beers in that repo are that brewery's beers — enforced by repo ownership, not by policy.
  4. Collab beers list collaborator DIDs, and each collaborator confirms with a beer.bottlecaps.collab record in its own repo.
  5. Beers from repos without a brewery record are unverified community submissions (breweryName fills in the display name).

Check-ins #

beer.bottlecaps.checkin #

A log of drinking a beer. Fields:

  • beer (required) — strong ref to a beer.bottlecaps.beer record
  • rating — half-star increments, 1–10 (10 = 5 stars)
  • notes — tasting notes
  • serving — draft, bottle, can, cask, growler, crowler, taster
  • location — union of the community location lexicons: community.lexicon.location.address / fsq / geo / hthree (same pattern as community.lexicon.calendar.event)
  • photo — an image blob
  • createdAt (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.