Read AT Protocol posts without an AppView. Spec and reference code. 鉱石ラヂオモデル:AppView に頼らずに投稿を読む仕組み

Files
README.md

crystal-radio #

The crystal radio model: read AT Protocol posts without relying on an AppView.

A crystal radio has no battery. The radio wave powers it. Tune to a frequency and you hear only that signal. This repository applies the same idea to a Bluesky client. No server that collects everyone's posts. No server of its own. The first screen appears before any network request.

Originator: Nighthaven (@moja.blue) · used in blusco ∅

日本語

Four parts #

Part What it does
Shelf The last timeline, stored on the device. Drawn before any network request.
Well Ask the PDS of each account you chose to read with com.atproto.repo.listRecords. All at once.
Weir While the client is open, receive those accounts' new posts from Jetstream. About one second after they are written.
Dam The AppView. Only for finding people and reverse lookups (replies, likes, quotes).

The timeline shows posts written by known accounts. That is a forward lookup, and every PDS can answer it. Reverse lookups need a server that collects every PDS. So the path to the first screen uses forward lookups only.

The full rules are in SPEC.md (Japanese: SPEC.ja.md). A plain explanation for a general reader: The Crystal Radio Model.

Measurements #

36 accounts on 15 hosts, measured from a Mac on 2026-10-23.

Path Until all 36 arrive
Dam: getAuthorFeed, 6 in parallel 2.2–2.5 s
Well: listRecords, all at once 1.1–1.9 s
Weir: replay 1 hour 1.8 s
Weir: replay 12 hours 10 s

On iPhone, blusco ∅ draws the shelf at 0.00 s and the well's posts at 1.1–1.8 s.

Files #

Path What it is
SPEC.md, SPEC.ja.md The specification
reference/web/crystal-radio.js Well and weir for the browser. No dependencies.
reference/swift/CrystalRadio.swift Well and weir in Swift. Foundation only.
tools/measure.mjs Measures dam, well, and weir for a list of DIDs. Prints counts and times only.

The reference code is taken from blusco ∅. The shelf is left to the client, because each client stores its own timeline.

node tools/measure.mjs dids.txt well
node tools/measure.mjs dids.txt weir 6h

Current limit: post contents still come from the AppView #

The well and the weir find which posts exist without an AppView. To show a post, this version still fetches its contents with app.bsky.feed.getPosts, 25 at a time. So:

  • If the AppView stops, the shelf still appears, and the well and the weir still find new posts. Their contents do not arrive until the AppView returns.
  • The reference code takes getPosts as a function you pass in. A client can replace it.

A record alone is not enough to replace the AppView:

From the AppView From the record alone
Author name and avatar From a profile cache on the device
Images com.atproto.sync.getBlob on the author's PDS
Whether you liked it From your own like records
Quoted post One more request to the quoted author's PDS
Labels (spam, adult, etc.) and the hide/blur decision Not available. Labels arrive through the AppView.
Muted accounts Not available. Mutes live in AppView preferences.
Whether it has replies Not available. That is a reverse lookup.

So drawing from records is a fallback for when the AppView does not answer, not a replacement. blusco ∅ (iPhone) has this fallback since 2026-10-23: names and avatars come from a device cache, images from getBlob, images stay blurred until tapped (labels unchecked), muted accounts are removed using the last mute list read from the AppView, and quotes and videos are skipped. When the AppView returns, those posts are fetched again. The reference code here does not include the fallback yet.

Other limits are listed in SPEC 9: word windows and other people's custom feeds stay with the AppView, the weir depends on the Relay, and Jetstream replay reached 36 hours back on the host measured.

License #

  • Code (reference/, tools/): MIT
  • Specification (SPEC.md, SPEC.ja.md): CC BY 4.0