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
getPostsas 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 #
- Swift 61%
- JavaScript 39%