Core Concepts
This page covers the two models you need to understand before the SDK code makes sense: how AT Protocol handles identity and data storage, and how Scribe organises content on top of it.
AT Protocol basics
Section titled “AT Protocol basics”Scribe is built on the AT Protocol — the same open protocol that powers Bluesky. You don’t need to know the protocol in depth, but a few concepts come up constantly in the SDK.
Handle
Section titled “Handle”A handle is a human-readable username — for example, alice.bsky.social or anthonycregan.dev. Authors use their handle to identify themselves.
Handles are convenient but not stable. An author can change their handle at any time. For this reason, the SDK always resolves a handle to a DID before fetching any data.
DID (Decentralized Identifier)
Section titled “DID (Decentralized Identifier)”A DID is a permanent, globally unique identifier — for example, did:plc:abc123. Where a handle is like a display name, a DID is like a passport number: it never changes.
There are two DID types you may encounter:
| Type | Example | How it works |
|---|---|---|
did:plc | did:plc:abc123 | Managed by the PLC directory at plc.directory — the most common type for accounts on bsky.social |
did:web | did:web:anthonycregan.dev | Derived from a domain name; the DID document is fetched from https://{domain}/.well-known/did.json |
The SDK accepts either a handle or a DID as the author argument to all fetch functions. Handles are resolved to DIDs automatically.
PDS (Personal Data Server)
Section titled “PDS (Personal Data Server)”The PDS is the server that hosts an author’s data. All Scribe content — sites, groups, articles — lives on the author’s PDS. It may be a shared provider (like bsky.social) or self-hosted.
The SDK discovers the correct PDS for any author by fetching their DID document and reading the #atproto_pds service endpoint. This is handled automatically — you never need to look up a PDS URL yourself.
Collections and AT URIs
Section titled “Collections and AT URIs”Data on a PDS is organised into collections — namespaced buckets of records. Scribe uses two:
| Collection | Contains |
|---|---|
site.standard.publication | Site records (one per site the author manages) |
site.standard.document | Article records (draft, unpublished, and published) |
Each record within a collection has an rkey (record key) — a unique identifier within that collection. For Scribe, both article and publication rkeys are opaque TIDs (e.g. 3mp4nd46xwr2h) assigned by the AT Protocol. An article’s human-readable slug is stored in the record’s path field, not in the rkey.
A record’s full address is an AT URI:
at://{did}/{collection}/{rkey}at://did:plc:abc123/site.standard.publication/3mp4nd46xwr2hat://did:plc:abc123/site.standard.document/3mp47vvkh342nThe Scribe content model
Section titled “The Scribe content model”On top of AT Protocol, Scribe defines a simple hierarchy for organising content.
A Site is an author’s publication — the top-level container for all their content. It is stored as a single record in the site.standard.publication collection.
A Site carries:
- Metadata:
title,description,url(domain),urlPrefix(optional path prefix),logoImageUrl,splashImageUrl,icon(blob) - A list of Groups (published content)
- A list of ungrouped articles (unpublished content)
The url and urlPrefix together define where the site’s content lives:
url: "anthonycregan.co.uk"urlPrefix: "blog"→ site root: anthonycregan.co.uk/blog→ article: anthonycregan.co.uk/blog/{group-slug}/{article-slug}When urlPrefix is empty, groups are immediate children of the domain:
url: "norobots.blog"urlPrefix: ""→ article: norobots.blog/{group-slug}/{article-slug}Looking up a publication
Section titled “Looking up a publication”The SDK looks up a publication record by the site’s canonical HTTPS URL. Pass the full URL as the second argument to fetchSite:
await fetchSite('alice.bsky.social', 'https://alice.bsky.social');await fetchSite('anthonycregan.dev', 'https://norobots.blog');Internally the SDK scans the author’s site.standard.publication collection and finds the record whose url field matches. Publication rkeys are opaque TIDs (3mp4nd46xwr2h) — you do not need to know or construct them.
The scribe.slug field still exists on publication records and is used by Scribe CMS for routing and display purposes, but the public SDK API works entirely from the canonical URL.
A Group is a named, ordered collection of articles within a Site — a section or category. Groups have a slug (URL segment) and a title. Their order within the Site is significant.
Article
Section titled “Article”An Article is a single piece of written content. All articles — whether draft, unpublished, or published — are stored in the site.standard.document collection. An article’s publication state is determined entirely by whether it appears in a Site record (see Publication states below), not by which collection it lives in. It contains:
title,description,content(full HTML),textContent(plain text),path(URL path),coverImageUrltags— optional array of author-defined freeform strings for categorisationcontributors— optional array ofArticleContributorobjects (DID, role, displayName)site— AT URI of the publication record, e.g.at://did:plc:…/site.standard.publication/3abccanonicalUrl— fully-qualified article URL, e.g.https://myblog.com/blog/creative-writing/my-articlebskyPostRef— Bluesky post AT URI and CID, present if the article was cross-posted via Scribe CMSpublishedAt,updatedAttimestamps
The Site record references articles, not the other way around.
ArticleRef
Section titled “ArticleRef”Fetching a Site gives you the full list of articles without making additional requests — because the Site record contains ArticleRefs: lightweight cached snapshots of each article’s metadata (title, slug, description, splash image, tags). Only the full content field is excluded.
This avoids N+1 fetch patterns: you can render an article list from the Site record alone, then fetch individual articles on demand.
Publication states
Section titled “Publication states”Every article is in one of three states:
| State | Condition | What it means |
|---|---|---|
| Draft | Exists on the author’s PDS; not referenced in any Site record | The author hasn’t associated it with any site yet |
| Unpublished | Referenced in a Site’s ungroupedArticles | Belongs to a site but not yet placed in a Group |
| Published | Referenced in a Group within a Site | Has a canonical URL on the author’s consumer site |
The SDK returns ungrouped articles as site.ungroupedArticles and published articles inside site.groups[n].articles. How you present each state is up to you.
How the SDK uses this
Section titled “How the SDK uses this”When you call fetchSite("alice.bsky.social", "https://alice.bsky.social"):
- The SDK resolves
alice.bsky.social→ a DID (viaplc.directoryor the handle’s DNS record) - It fetches the DID document to discover Alice’s PDS URL
- It scans Alice’s
site.standard.publicationcollection and finds the record matching the URL - It returns a typed
Siteobject with groups and article refs already embedded
The PDS URL is cached in memory for the lifetime of the page load, so repeated fetches for the same author don’t re-fetch the DID document.
Next steps
Section titled “Next steps”Now that you have the model in your head, the Quickstart shows you how to fetch your first site and article in under 5 minutes.