OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-social commits / 26f6004

Add ATPROTO-P2P-BRIDGE design doc
P2P-native AT-Proto on Hypercore: autobase-commons lens + edge bridge. did:plc identity (secp256k1 derived from the same keet-identity-key BIP39 seed as our Ed25519 stack), repo=canonical MST (Hyperbee is only storage substrate), little-heap sync, edge HTTP bridge. Keet fast-forward behaviour verified against keet-core 4.17. Distilled in hyper-fleeky-dev #85. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
26f6004def70e02855dc080e9ee637bee8d17209
by plankatron <fleeky@gmail.com> · 2026-06-20T02:42:57.000Z

Changed files (1) +303 -0

docs/ATPROTO-P2P-BRIDGE.md +303 -0
……@@ -0,0 +1,303 @@
1# AT Protocol over Hypercore — P2P-native design + bridge
2
3Companion to [ATPROTO-BRIDGE.md](ATPROTO-BRIDGE.md). That doc covers the
4*Tier-2 bridge* (Keet rooms ↔ bsky posts) and the PDS/POSSE building
5blocks. **This** doc is the deeper architecture: build the real thing
6**P2P-native on Hypercore**, run the AppView as an **autobase commons**,
7sync in a **little-heap** way, and treat bsky as *one network to bridge
8to at the edge* — not the home.
9
10Status: **design / research synthesis** (2026-06-19). Nothing here is
11built 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
18AT Proto's **data model is already P2P-shaped**: a user's repo is a
19signed, append-only, hash-linked commit log = a **Hypercore**; the
20record tree (MST) = a **Hyperbee**; the signing key = the hypercore
21keypair. What is *not* P2P is the **transport** (HTTP/WebSocket), the
22**aggregation** (a central relay + giant AppView), and **identity
23resolution** (DNS/PLC). So: adopt AT Proto's *data model and lexicons*,
24run them on a **little-heap Hypercore transport** (direct peer
25replication over Hyperswarm, scoped autobase views), and put a thin,
26replaceable **HTTP bridge at the edge** so the bsky world can see us and
27we can see it. We do **not** adopt AT Proto's big heap.
28
29---
30
31## 2. The three layers — keep them separate
32
33Conflating 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
42DID ≠ 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
68bsky transfers blobs as **whole-file HTTP** (`uploadBlob` POST →
69CID; `getBlob` GET by CID — CIDv1, raw, sha-256). There is **no chunk
70protocol** on the wire. So the "bridge" is a CID-addressed HTTP facade
71over our content store: compute the sha-256 CID, index `cid → bytes`,
72serve `getBlob`. hyper-overlay (if used) is purely the *node-to-node
73backhaul 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
81resolved via the PLC directory. No domain needed for the DID itself;
82multi-user is trivial (everyone gets their own). **Crypto caveat:**
83did:plc signs with **ECDSA (secp256k1 / P-256)** but keet-identity-key is
84**Ed25519** — not interchangeable. Anchor on the **BIP39 seed**, not the
85Ed25519 key: from one mnemonic derive *both* an Ed25519 key (our
86Hypercore world) and a secp256k1 key (did:plc rotation + atproto repo
87signing) → one recovery phrase recovers both, user-owned, bsky-compatible.
88`hyper-identity`'s `IdentityRegistry` stores the binding (new `atproto`
89provider); the did:plc/secp256k1/MST protocol logic lives in a companion
90module, not in IdentityRegistry.
91
92The PLC directory is being spun out to a neutral Swiss association +
93gaining a mirroring API; we can also mirror it locally for our own
94reads.
95
96### The handle is the stubborn part (a *separate* hostname problem)
97
98A handle must be a **hostname** — paths are illegal, one handle per
99account (upstream rejected multi-handle, [#2206]). So per-user *verified
100pretty* 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
109So: **identity without a subdomain is solved (did:plc); a verified
110pretty handle is not** — it's bare-apex (1), BYO-domain, unverified, or
111subdomain (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
128The relay + global firehose is AT Proto's **default**, not its only
129mode. The primitives support little-heap:
130
1311. **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.
1352. **Filtered firehose** — Jetstream filters by **collection (record
136 type) + author DID**. **Not by tag/content** — that requires a local
137 index pass.
1383. **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).
1424. Full-network mirroring is the **exception** (bsky's own guidance) —
143 only if you truly want a complete mirror.
144
145Our Hypercore core goes **fully little-heap**: repos replicate **directly
146peer-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
152An AppView is a **stream-fed materialized view** (counts, threads,
153follower graph, timelines, topical feeds). That is *exactly* autobase:
154`apply()` is the indexer, the linearized **Hyperbee view** is the
155queryable index, multiple writers = collaborative/sharded ingest.
156
157**Scale honesty:** autobase `apply()` is single-threaded and replays on
158boot, so this is a **community/topic-scoped** lens (the AppViewLite /
159Constellation niche), **not** a full-network AppView (that's ScyllaDB
160territory). That limit is the point: you never *need* the global lens.
161
162**Why "commons" matters (the decentralization argument that actually
163holds):** a central AppView is a *service you query* (hosted, gated,
164winner-take-all by aggregation economics — a "glass castle"). An
165autobase 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
173So: don't build a smaller castle — make community-scale lenses into
174**replicable commons**. Many small panes, none winner-take-all because
175none is trying to be global.
176
177### Feed generators = the unit of a commons
178
179A feed generator is a small, single-topic index (e.g. "gamedev"). Build
180it as an autobase commons: a few members consume the (text-only,
181Pi-scale) post stream and filter by tag/content; the resulting index is
182the autobase view; everyone else **replicates** it. **History**: the
183commons backfills/accumulates **once**; a new member **fast-forwards to a
184signed view checkpoint** (autobase `fastForwardTo`) — syncing the current
185materialized index, *not* replaying all history — so joining is cheap and
186no per-operator re-backfill is needed (vanilla AT Proto feeds are islands
187that each rebuild the past). This is **verified keet behaviour**, not
188aspiration — see §9.
189
190---
191
192## 7. The bridge — P2P core, replaceable HTTP membrane
193
194The core (repos, autobase lens, Hyperswarm discovery, our identity) is
195fully functional with **no HTTP, no relay, no bsky**. The bridge is a
196two-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**.
205Kill it and the P2P network is fine — only bsky-interop pauses until
206another bridge spins up. So it cannot become a new castle. (Same spirit
207as 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
212your own relay (what Blacksky does with `atproto.africa`) or federate via
213a friendlier third-party relay; in pure P2P there's no relay, so the cap
214only applies at the optional main-bsky bridge.
215
216---
217
218## 8. Build order
219
2200. did:plc account-creation flow + keet-identity-key rotation anchoring.
2211. Repo-as-Hypercore: port MST/CAR/DAG-CBOR (millipds / `@atproto/repo`)
222 onto Hyperbee + hyperblobs.
2232. PDS-HTTP gateway (`subscribeRepos`, `getRepo`, `getRecord`, `getBlob`,
224 `server.*`) on a tabertron service, routed via 7rnx (`--root-path`).
2253. requestCrawl a relay (Blacksky first — did:web-friendlier; then
226 bsky.network with did:plc). Prove a real follow end-to-end.
2274. Autobase backlink index (Constellation-shaped) → counts, threads,
228 follower graph → topical feed-generator commons.
2295. 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