Build your own frontend
SDK concepts
This page covers the two models the SDK is built on: how AT Protocol handles identity and data storage, and how content is organised on top of it. If you've read Core concepts, the identity part will be familiar — this page adds the record-level detail the SDK exposes.
One vocabulary, three name-sets
The SDK's API predates SkyScribe's naming. It uses the AT Protocol names, so throughout this section:
| This guide (SkyScribe) | The SDK | AT Protocol record |
|---|---|---|
| Publication | Site (fetchSite) | site.standard.publication |
| Document | Article (fetchArticle) | site.standard.document |
| Category | Group | scribe.groups[] |
The rest of this page — and the framework guides, SDK guides, and API reference — all use the SDK's column.
AT Protocol basics
SkyScribe 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
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)
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)
The PDS is the server that hosts an author's data. All published 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.
Tip
This PDS resolution is why the SDK works correctly for self-hosted instances, not just bsky.social accounts.
Collections and AT URIs
Data on a PDS is organised into collections — namespaced buckets of records. SkyScribe uses two:
| Collection | Contains |
|---|---|
site.standard.publication | Site records (one per site the author manages) |
site.standard.document | Article records (draft and published) |
Each record within a collection has an rkey (record key) — a unique identifier within that collection. For SkyScribe, 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/3mp4nd46xwr2h
at://did:plc:abc123/site.standard.document/3mp47vvkh342n
The content model
On top of AT Protocol, SkyScribe defines a simple hierarchy for organising content.
Site
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 — legacy field, kept for backwards compatibility with content predating the current publish flow; expect it to be empty for anything published with current tooling (see Publication states)
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
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 SkyScribe for routing and display purposes, but the public SDK API works entirely from the canonical URL.
Group
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
An Article is a single piece of written content. All articles — whether draft 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's Groups (see Publication states below), not by which collection it lives in. An article is associated with at most one Site at a time. 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 SkyScribepublishedAt,updatedAttimestamps
The Site record references articles, not the other way around.
Note
site.standard.document records follow the site.standard lexicon. Any SkyScribe-specific data that is not part of the spec — such as the article's creation date and internal tracking fields — is stored inside a scribe extension object on the raw PDS record. The SDK maps all of this onto the flat Article type transparently; you do not need to access the scribe object directly.
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.
Note
ArticleRef is a code-level concept for developers. It is not exposed in the SkyScribe UI — authors just see their articles.
Publication states
Every article is in one of two 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 |
| Published | Referenced in a Group within a Site | Has a canonical URL on the author's consumer site, assigned to that site and placed in a group in one step |
The SDK returns published articles inside site.groups[n].articles. Drafts are never returned by fetchSite at all — since a draft isn't referenced by any Site record, there's nothing for the SDK to find it by.
Note
site.ungroupedArticles still exists on the Site type for backwards compatibility with content published before this two-state model, but current SkyScribe versions never write to it — expect an empty array. Don't build UI around it for new integrations.
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
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.