| … | … | @@ -0,0 +1,303 @@ |
| 1 | # AT Protocol over Hypercore — P2P-native design + bridge |
| 2 | |
| 3 | Companion to [ATPROTO-BRIDGE.md](ATPROTO-BRIDGE.md). That doc covers the |
| 4 | *Tier-2 bridge* (Keet rooms ↔ bsky posts) and the PDS/POSSE building |
| 5 | blocks. **This** doc is the deeper architecture: build the real thing |
| 6 | **P2P-native on Hypercore**, run the AppView as an **autobase commons**, |
| 7 | sync in a **little-heap** way, and treat bsky as *one network to bridge |
| 8 | to at the edge* — not the home. |
| 9 | |
| 10 | Status: **design / research synthesis** (2026-06-19). Nothing here is |
| 11 | built yet. Several entries correct earlier assumptions — see |
| 12 | [Corrections](#corrections-things-that-look-true-but-arent). |
| 13 | |
| 14 | --- |
| 15 | |
| 16 | ## 1. The one-paragraph thesis |
| 17 | |
| 18 | AT Proto's **data model is already P2P-shaped**: a user's repo is a |
| 19 | signed, append-only, hash-linked commit log = a **Hypercore**; the |
| 20 | record tree (MST) = a **Hyperbee**; the signing key = the hypercore |
| 21 | keypair. What is *not* P2P is the **transport** (HTTP/WebSocket), the |
| 22 | **aggregation** (a central relay + giant AppView), and **identity |
| 23 | resolution** (DNS/PLC). So: adopt AT Proto's *data model and lexicons*, |
| 24 | run them on a **little-heap Hypercore transport** (direct peer |
| 25 | replication over Hyperswarm, scoped autobase views), and put a thin, |
| 26 | replaceable **HTTP bridge at the edge** so the bsky world can see us and |
| 27 | we can see it. We do **not** adopt AT Proto's big heap. |
| 28 | |
| 29 | --- |
| 30 | |
| 31 | ## 2. The three layers — keep them separate |
| 32 | |
| 33 | Conflating these caused most of the confusion. They are independent: |
| 34 | |
| 35 | | Layer | AT Proto term | What it is | Our mapping | |
| 36 | |---|---|---|---| |
| 37 | | **Identity** | DID | permanent, portable ID + signing key + PDS pointer | `did:plc` (see §4); Hypercore key natively | |
| 38 | | **Handle** | handle | the human @username | a **hostname** — the stubborn part (§4) | |
| 39 | | **Data home** | PDS | where the repo + blobs live | Hyperbee (records) + hyperblobs (blobs), P2P-replicated | |
| 40 | | **Lens** | AppView | the read model / index | an **autobase commons** (§6) | |
| 41 | |
| 42 | DID ≠ handle ≠ PDS ≠ AppView. You can switch the *lens* without touching |
| 43 | *data*; you can move *data* (PDS) without changing *identity* (DID). |
| 44 | |
| 45 | --- |
| 46 | |
| 47 | ## 3. Storage — what backs a repo |
| 48 | |
| 49 | - **Records** (posts, likes, follows = "collections", i.e. record |
| 50 | *types*) → the canonical atproto repo is a **Merkle Search Tree** |
| 51 | whose authenticity *is* its exact CIDs + signed commits. **A Hyperbee |
| 52 | is NOT an MST** (it's a B-tree; it does not produce atproto CIDs) — |
| 53 | Hyperbee/hypercore is at most the *storage substrate*; the MST + |
| 54 | DAG-CBOR + CAR + commit-signing must be ported faithfully |
| 55 | (`@atproto/repo` / `millipds`, §10). Byte-exact CIDs are the crux (§9). |
| 56 | - **Blobs** (images, video — content-addressed by CID) → **hyperblobs** |
| 57 | (content-addressed blob store; what hypernote already uses for media). |
| 58 | - **Not Hyperdrive** — blobs are hash-addressed, not path-addressed. |
| 59 | - **hyper-overlay** is a chunk-aware *file-sync* layer (≈ |
| 60 | Hyperdrive-with-dedup + multi-source); use it only for *large/ |
| 61 | versioned media* where FastCDC chunk-dedup pays. For ordinary post |
| 62 | blobs, hyperblobs is the cleaner primitive. |
| 63 | - A Hypercore is already a signed append-only log → **record |
| 64 | authenticity is intrinsic** (the core key *is* the repo signing key). |
| 65 | |
| 66 | ### Blob bridge (if we expose to bsky) |
| 67 | |
| 68 | bsky transfers blobs as **whole-file HTTP** (`uploadBlob` POST → |
| 69 | CID; `getBlob` GET by CID — CIDv1, raw, sha-256). There is **no chunk |
| 70 | protocol** on the wire. So the "bridge" is a CID-addressed HTTP facade |
| 71 | over our content store: compute the sha-256 CID, index `cid → bytes`, |
| 72 | serve `getBlob`. hyper-overlay (if used) is purely the *node-to-node |
| 73 | backhaul behind* the facade, never the bsky-facing protocol. |
| 74 | |
| 75 | --- |
| 76 | |
| 77 | ## 4. Identity — did:plc, and the handle reality |
| 78 | |
| 79 | **Use `did:plc`.** It is the only AT-Proto-blessed DID method that is |
| 80 | **subdomain-free and path-free** — a self-authenticating, hash-based ID |
| 81 | resolved via the PLC directory. No domain needed for the DID itself; |
| 82 | multi-user is trivial (everyone gets their own). **Crypto caveat:** |
| 83 | did:plc signs with **ECDSA (secp256k1 / P-256)** but keet-identity-key is |
| 84 | **Ed25519** — not interchangeable. Anchor on the **BIP39 seed**, not the |
| 85 | Ed25519 key: from one mnemonic derive *both* an Ed25519 key (our |
| 86 | Hypercore world) and a secp256k1 key (did:plc rotation + atproto repo |
| 87 | signing) → one recovery phrase recovers both, user-owned, bsky-compatible. |
| 88 | `hyper-identity`'s `IdentityRegistry` stores the binding (new `atproto` |
| 89 | provider); the did:plc/secp256k1/MST protocol logic lives in a companion |
| 90 | module, not in IdentityRegistry. |
| 91 | |
| 92 | The PLC directory is being spun out to a neutral Swiss association + |
| 93 | gaining a mirroring API; we can also mirror it locally for our own |
| 94 | reads. |
| 95 | |
| 96 | ### The handle is the stubborn part (a *separate* hostname problem) |
| 97 | |
| 98 | A handle must be a **hostname** — paths are illegal, one handle per |
| 99 | account (upstream rejected multi-handle, [#2206]). So per-user *verified |
| 100 | pretty* handles fundamentally need a hostname each: |
| 101 | |
| 102 | | Handle option (no subdomain) | bsky accepts? | Verified @name? | Multi-user? | |
| 103 | |---|---|---|---| |
| 104 | | Bare apex `@7rnx.net` | ✅ | ✅ | ❌ one user | |
| 105 | | Bring-your-own domain | ✅ | ✅ | ✅ (user owns a domain) | |
| 106 | | Invalid / no handle | ✅ followable by DID | ❌ shows ⚠ | ✅ but all ⚠ | |
| 107 | | Subdomain `@alice.7rnx.net` | ✅ | ✅ | ✅ — needs wildcard cert **or** cert-free DNS-TXT | |
| 108 | |
| 109 | So: **identity without a subdomain is solved (did:plc); a verified |
| 110 | pretty handle is not** — it's bare-apex (1), BYO-domain, unverified, or |
| 111 | subdomain (wildcard TLS cert, or DNS-TXT with delegated DNS control). |
| 112 | |
| 113 | ### 7rnx.net facts (verified live) |
| 114 | |
| 115 | - Wildcard DNS `*.7rnx.net` is **already live** (`142.93.23.112`). |
| 116 | - TLS cert is **apex-only** (`CN=7rnx.net`, SAN `DNS:7rnx.net`); cert |
| 117 | auto-renews via certbot **apache/HTTP-01**. |
| 118 | - A `*.7rnx.net` wildcard cert requires **DNS-01**; DNS is at HostMonster |
| 119 | (no clean ACME API) → use **CNAME delegation of `_acme-challenge`** to |
| 120 | acme-dns, leaving the apex cert untouched (additive, reversible). |
| 121 | - Path-based did:web (`did:web:7rnx.net:u:alice`) serves cert-free off |
| 122 | the apex — **but AT Proto rejects it** (see Corrections). Dead end. |
| 123 | |
| 124 | --- |
| 125 | |
| 126 | ## 5. Sync — little heap, not big heap |
| 127 | |
| 128 | The relay + global firehose is AT Proto's **default**, not its only |
| 129 | mode. The primitives support little-heap: |
| 130 | |
| 131 | 1. **Direct PDS subscription** — subscribe to a single PDS's |
| 132 | `com.atproto.sync.subscribeRepos` *without a relay*. Same API. Peers |
| 133 | pulling from the specific servers of people they follow = the |
| 134 | keet/mastodon model. |
| 135 | 2. **Filtered firehose** — Jetstream filters by **collection (record |
| 136 | type) + author DID**. **Not by tag/content** — that requires a local |
| 137 | index pass. |
| 138 | 3. **Selective backfill** — Sync 1.1 / `Tap` fetch specific collections |
| 139 | / path-ranges of specific repos, verifiable. You **stream-filter and |
| 140 | store only the slice** (storage stays tiny); you only over-*fetch* |
| 141 | when doing global content filtering (no source-side tag filter). |
| 142 | 4. Full-network mirroring is the **exception** (bsky's own guidance) — |
| 143 | only if you truly want a complete mirror. |
| 144 | |
| 145 | Our Hypercore core goes **fully little-heap**: repos replicate **directly |
| 146 | peer-to-peer over Hyperswarm**, no relay, no global firehose internally. |
| 147 | |
| 148 | --- |
| 149 | |
| 150 | ## 6. The AppView as an autobase commons (lens, not castle) |
| 151 | |
| 152 | An AppView is a **stream-fed materialized view** (counts, threads, |
| 153 | follower graph, timelines, topical feeds). That is *exactly* autobase: |
| 154 | `apply()` is the indexer, the linearized **Hyperbee view** is the |
| 155 | queryable index, multiple writers = collaborative/sharded ingest. |
| 156 | |
| 157 | **Scale honesty:** autobase `apply()` is single-threaded and replays on |
| 158 | boot, so this is a **community/topic-scoped** lens (the AppViewLite / |
| 159 | Constellation niche), **not** a full-network AppView (that's ScyllaDB |
| 160 | territory). That limit is the point: you never *need* the global lens. |
| 161 | |
| 162 | **Why "commons" matters (the decentralization argument that actually |
| 163 | holds):** a central AppView is a *service you query* (hosted, gated, |
| 164 | winner-take-all by aggregation economics — a "glass castle"). An |
| 165 | autobase view is an *artifact you replicate*: |
| 166 | |
| 167 | | Centralizing force | Autobase commons answer | |
| 168 | |---|---| |
| 169 | | Aggregation is a cost moat → one operator | index **co-built by many writers**; cost pooled | |
| 170 | | You're a gated client of their lens | readers **replicate the index** and read locally | |
| 171 | | Editorial choices are a black box | deterministic `apply()` over a signed log → **verifiable + forkable** | |
| 172 | |
| 173 | So: don't build a smaller castle — make community-scale lenses into |
| 174 | **replicable commons**. Many small panes, none winner-take-all because |
| 175 | none is trying to be global. |
| 176 | |
| 177 | ### Feed generators = the unit of a commons |
| 178 | |
| 179 | A feed generator is a small, single-topic index (e.g. "gamedev"). Build |
| 180 | it as an autobase commons: a few members consume the (text-only, |
| 181 | Pi-scale) post stream and filter by tag/content; the resulting index is |
| 182 | the autobase view; everyone else **replicates** it. **History**: the |
| 183 | commons backfills/accumulates **once**; a new member **fast-forwards to a |
| 184 | signed view checkpoint** (autobase `fastForwardTo`) — syncing the current |
| 185 | materialized index, *not* replaying all history — so joining is cheap and |
| 186 | no per-operator re-backfill is needed (vanilla AT Proto feeds are islands |
| 187 | that each rebuild the past). This is **verified keet behaviour**, not |
| 188 | aspiration — see §9. |
| 189 | |
| 190 | --- |
| 191 | |
| 192 | ## 7. The bridge — P2P core, replaceable HTTP membrane |
| 193 | |
| 194 | The core (repos, autobase lens, Hyperswarm discovery, our identity) is |
| 195 | fully functional with **no HTTP, no relay, no bsky**. The bridge is a |
| 196 | two-way translator at the boundary: |
| 197 | |
| 198 | - **Outbound** (HTTP world sees us): present a standard PDS face + |
| 199 | `subscribeRepos` firehose for our repos; `requestCrawl` a relay; map |
| 200 | each user's Hypercore key → a `did:plc`. |
| 201 | - **Inbound** (we see the HTTP world): consume a (filtered) bsky firehose |
| 202 | into our autobase view. |
| 203 | |
| 204 | **Property:** the bridge is **stateless, replaceable, pluralizable**. |
| 205 | Kill it and the P2P network is fine — only bsky-interop pauses until |
| 206 | another bridge spins up. So it cannot become a new castle. (Same spirit |
| 207 | as wiredove: P2P core, thin HTTP face.) |
| 208 | |
| 209 | **Constraints inherited *only at the bridge*:** did:plc + handle rules |
| 210 | (§4), a relay dependency, and bsky.network's per-PDS account cap (now |
| 211 | ~100 default, was 10 in early access, rises with trust). Escape: run |
| 212 | your own relay (what Blacksky does with `atproto.africa`) or federate via |
| 213 | a friendlier third-party relay; in pure P2P there's no relay, so the cap |
| 214 | only applies at the optional main-bsky bridge. |
| 215 | |
| 216 | --- |
| 217 | |
| 218 | ## 8. Build order |
| 219 | |
| 220 | 0. did:plc account-creation flow + keet-identity-key rotation anchoring. |
| 221 | 1. Repo-as-Hypercore: port MST/CAR/DAG-CBOR (millipds / `@atproto/repo`) |
| 222 | onto Hyperbee + hyperblobs. |
| 223 | 2. PDS-HTTP gateway (`subscribeRepos`, `getRepo`, `getRecord`, `getBlob`, |
| 224 | `server.*`) on a tabertron service, routed via 7rnx (`--root-path`). |
| 225 | 3. requestCrawl a relay (Blacksky first — did:web-friendlier; then |
| 226 | bsky.network with did:plc). Prove a real follow end-to-end. |
| 227 | 4. Autobase backlink index (Constellation-shaped) → counts, threads, |
| 228 | follower graph → topical feed-generator commons. |
| 229 | 5. Ingress bridge: consume bsky firehose into our view. |
| 230 | |
| 231 | --- |
| 232 | |
| 233 | ## 9. Corrections (things that look true but aren't) |
| 234 | |
| 235 | - **Path-based did:web is NOT valid in AT Proto.** Spec: *"only |
| 236 | hostname-level did:web DIDs are supported; path-based DIDs are not |
| 237 | supported."* The deep-path `did:web:7rnx.net:u:alice` resolves under |
| 238 | the *generic* W3C algorithm (proven cert-free on 7rnx.net) but a real |
| 239 | relay/AppView **rejects it**. Use did:plc. |
| 240 | - **did:web "invisible in main AppView" ([#4379]) was a misconfigured |
| 241 | `did.json`** poisoning the initial identity event → actor row never |
| 242 | created, unrecoverable. Lesson: get the identity doc **exactly right |
| 243 | before requestCrawl**; the AppView recovers poorly from a bad first |
| 244 | event. |
| 245 | - **Blobs ≠ Hyperdrive.** Content-addressed → hyperblobs/overlay. |
| 246 | - **AppView-on-autobase ≠ full-network.** Community-scoped only. |
| 247 | - **Jetstream can't filter by tag/content** — only collection + DID. |
| 248 | - **Repo ≠ Hyperbee.** The canonical atproto repo is a Merkle Search Tree |
| 249 | (exact CIDs + signed commits); Hyperbee is a B-tree, only a storage |
| 250 | substrate. Byte-exact MST is the hardest unproven piece (§3). |
| 251 | - **did:plc keys are ECDSA, not Ed25519.** Derive the atproto |
| 252 | secp256k1/P-256 keys from the same BIP39 seed as the Ed25519 identity (§4). |
| 253 | - **Keet peers do NOT store every writer's full raw oplog.** Verified in |
| 254 | keet-core 4.17: a joining peer **fast-forwards** to a signed checkpoint |
| 255 | of the materialized view — `fastForwardTo` is first-class in room |
| 256 | create/join/migrate + the HRPC wire (`tasks/rooms/create.js`, |
| 257 | `schema/spec/api/hrpc/messages.js`); the client reads `view.db`/`view.blobs` |
| 258 | (`sync/loader.js`) and the sync engine emits `v2:room-fast-forward` |
| 259 | (`sync/updater.js`). "Holds the whole room" is true at the **view/state** |
| 260 | level, false at the **raw-oplog** level. This fast-forward is the |
| 261 | mechanism behind the cheap-join commons (§6). |
| 262 | |
| 263 | --- |
| 264 | |
| 265 | ## 10. Prior art / reading list |
| 266 | |
| 267 | **Minimal PDS / repo / MST / CAR (port this):** |
| 268 | - [millipds](https://github.com/DavidBuchanan314/millipds) (Python) — the |
| 269 | minimal from-scratch PDS; `atmst` + `dag-cbrrr`. The blueprint. |
| 270 | - [picopds](https://github.com/DavidBuchanan314/picopds) — even smaller |
| 271 | (single-repo) skeleton. |
| 272 | - [arroba](https://github.com/snarfed/arroba) — repo+MST+sync, powers |
| 273 | Bridgy Fed. |
| 274 | - [@atproto/repo](https://github.com/bluesky-social/atproto/tree/main/packages/repo) |
| 275 | (TS) — canonical MST/CAR/DAG-CBOR = spec-as-code; likely direct port |
| 276 | source for our Bare/JS stack. |
| 277 | - [bluepds](https://github.com/DrChat/bluepds) (Rust) — typed reference. |
| 278 | |
| 279 | **Lightweight sync:** |
| 280 | - [Jetstream](https://github.com/bluesky-social/jetstream) — filtered |
| 281 | JSON firehose (collection + DID). |
| 282 | - [Tap](https://docs.bsky.app/blog/introducing-tap) — backfill, cursor, |
| 283 | Sync 1.1, selective sync. |
| 284 | - [Sync spec](https://atproto.com/specs/sync). |
| 285 | - [microcosm-rs](https://github.com/at-microcosm/microcosm-rs) — |
| 286 | Constellation (backlink index) / Spacedust / Slingshot, Pi-scale. |
| 287 | |
| 288 | **Closest architectural twin (study most):** |
| 289 | - [Roomy](https://blog.muni.town/roomy-deep-dive/) (Muni Town) — **AT |
| 290 | Proto identity/discovery + Automerge CRDTs for P2P, local-first.** Our |
| 291 | exact problem, one substitution (Automerge → Hypercore/autobase) away. |
| 292 | Watch their **Beelay** protocol (E2E-encrypted multi-peer sync + |
| 293 | private data). |
| 294 | - [Jake Lazaroff — resilient local-first with atproto](https://jakelazaroff.com/words/building-more-resilient-local-first-software-with-atproto/). |
| 295 | |
| 296 | **Specs:** [identity](https://atproto.com/guides/identity) · |
| 297 | [handle](https://atproto.com/specs/handle) · |
| 298 | [did](https://atproto.com/specs/did) · |
| 299 | [repository](https://atproto.com/specs/repository) · |
| 300 | [data-model](https://atproto.com/specs/data-model). |
| 301 | |
| 302 | [#2206]: https://github.com/bluesky-social/atproto/discussions/2206 |
| 303 | [#4379]: https://github.com/bluesky-social/atproto/discussions/4379 |