| … | … | @@ -0,0 +1,251 @@ |
| 1 | # Retiring favela-web |
| 2 | |
| 3 | **Date:** 2026-09-29 · **Status:** spec, nothing built · **Follows:** #61, #62 §3, #73, `SOCIAL-MODEL.md` §6.7 |
| 4 | |
| 5 | favela-web is the July-era atproto appview. It is being retired, not maintained: on 2026-09-29 it |
| 6 | reached 20.3 GB on stortron for the third time, and the fix shipped (0.3.92) needed three releases |
| 7 | because its dependency closure is fragile. This document says where each of its jobs goes, what has |
| 8 | to be built, and in what order. |
| 9 | |
| 10 | The direction is the one `favela-identity/spec/identity.md` §6.3.1 already draws: favela-peer → |
| 11 | bridge:bsky (holds the key, signs) → keyless atproto-bridge. favela-web does not appear in it. |
| 12 | |
| 13 | --- |
| 14 | |
| 15 | ## 1. What favela-web does in production today |
| 16 | |
| 17 | Read from the live process on stortron (`/proc/<pid>/environ`), 2026-09-29. |
| 18 | |
| 19 | | # | Job | Live config | Fate | |
| 20 | |---|---|---|---| |
| 21 | | 1 | Public SSR site at `favela.7rnx.net` — an atproto appview over wj62 + e2jev | `FAVELA_ROSTER`, `FAVELA_PORT=4041` | → favela-surface, public read-only mode (§6) | |
| 22 | | 2 | The `loser-club` bsky custom feed (`getFeedSkeleton`, `describeFeedGenerator`) | `FAVELA_COMMUNITY_ROOMS=loser-club=91ce…` | → favela-bridge computes, hpp serves (§4, §5) | |
| 23 | | 3 | Inbound replies/likes from bsky (spacedust + constellation), retained ≤ 256 MB | `FAVELA_SPACEDUST=1`, `FAVELA_RETENTION_BUDGET` | → the bridge's `collect` (#73 §3); stops at retirement | |
| 24 | | 4 | Posting to wj62's repo through hpp's control socket | `FAVELA_HPP_SOCK` | Already superseded: favela-bridge projects favela-peer posts | |
| 25 | | 5 | Reading ActivityPub followers for display | `FAVELA_AP_BRIDGE` | Display only; stops, nothing lost | |
| 26 | | — | Labeler | **not live**: `FAVELA_LABELER_SEED` unset, `queryLabels` 404, no labeler service record | Built fresh (§5.2); nothing to migrate | |
| 27 | |
| 28 | atproto-bridge currently depends on favela-web — `FAVELA_WEB_UPSTREAM=http://127.0.0.1:4041`, |
| 29 | `pds/lib/favela-proxy.js:9-13`. **That is the wrong direction** and is removed by §5.3. |
| 30 | |
| 31 | --- |
| 32 | |
| 33 | ## 2. Ownership |
| 34 | |
| 35 | | Concern | Owner | Why there | |
| 36 | |---|---|---| |
| 37 | | Posts, communities, roles, curation, moderation decisions | **favela-peer** | the canonical records (`record-vocabulary.md` §0) | |
| 38 | | favela record → atproto (posts, skeletons, label intents) | **favela-bridge** | the only component allowed to know both sides (#61 §10 boundary). Owns `tidFor(name)` | |
| 39 | | Keys; signing commits **and labels** | **hyper-proto-peer** | holds wqnwul's key, which IS its PLC `#atproto_label` key (derived did:key matches) | |
| 40 | | Serving to bsky, keyless | **atproto-bridge** | reaches hpp over P2P only; no HTTP upstreams | |
| 41 | | Pages | **favela-surface** | renderer over favela-peer's socket | |
| 42 | |
| 43 | Retired after parity (§8): **favela-web**, and the old community stack — **favela-community** |
| 44 | (`CommunityRoom`, hyper-rooms on autobase, DID-keyed) and **favela-node**. |
| 45 | |
| 46 | ⛔ **The record→at:// mapping is a function, not state.** `favela-bridge/lib/tid.js:14-17`: the |
| 47 | bridge "holds no record→rkey map". An at:// URI is `at://<designated DID>/app.bsky.feed.post/` + |
| 48 | `tidFor(name)`. It is one-way (`tid.js:148-151`). Nothing in this spec stores it. |
| 49 | |
| 50 | --- |
| 51 | |
| 52 | ## 3. The community is a favela-peer room — no new op types |
| 53 | |
| 54 | `favela-peer/lib/room.js` implements keet's contract (#62 §3; commit `58f4fed`), and |
| 55 | `5920678`: *"a community is a room, not a type"*. Its op set is closed and its config has five keys |
| 56 | (`room.js:59-61`); **anything else is a message by convention**. This spec adds no op. |
| 57 | |
| 58 | ### 3.1 `_hs:room-meta` carries the community's projection |
| 59 | |
| 60 | The pinned, admin-authored `_hs:room-meta` message (`ROOMS-ARE-THE-COMPUTER.md:42,51,69-70`) |
| 61 | already has `type`, `discoverable` and `bridgedTo`. It gains: |
| 62 | |
| 63 | ```json |
| 64 | {"_hs":"room-meta","type":"community","public":true, |
| 65 | "bridgedTo":[{"platform":"bsky","did":"did:plc:wqnwulql7ndcz6xmtvaovl6u","feed":"loser-club"}], |
| 66 | "curation":{"curator":"roster","params":{}}} |
| 67 | ``` |
| 68 | |
| 69 | - **`public`** — the room's content may be projected and rendered to non-members. Rooms carry no |
| 70 | visibility today ("visibility is a place", `room.js:29`), so without this flag nothing in a room is |
| 71 | public. `record-vocabulary.md` §3.1: only public rooms get an at:// projection. |
| 72 | - **`bridgedTo[bsky]`** — the community DID and feed rkey. This replaces favela-community's |
| 73 | `meta:did`. |
| 74 | - **`curation`** — favela-web's contract (`curators.js:35-47`: `roster` | `subject`), moved verbatim. |
| 75 | |
| 76 | ### 3.2 ⛔ The role check is the reader's, and it is mandatory |
| 77 | |
| 78 | A message is gated at **peer** in apply (`room.js:329`), not at admin or moderator. So every reader |
| 79 | of a conventional message must check the author's role itself, via the room's existing |
| 80 | `roleOf`/`members` (`room.js:477-485`): |
| 81 | |
| 82 | - `_hs:room-meta` is honoured only from an **admin**; the newest admin-authored one wins. |
| 83 | - `_hs:mod-label` (§5.2) is honoured only from a **moderator or admin** of the moderation room. |
| 84 | |
| 85 | One shared function does this check, and favela-bridge calls favela-peer for it — never a local |
| 86 | re-derivation of roles. |
| 87 | |
| 88 | --- |
| 89 | |
| 90 | ## 4. favela-bridge gains a room pass |
| 91 | |
| 92 | Today it projects only the node's own feed (`peer.js:1006-1023`; nothing calls `room-*`). |
| 93 | |
| 94 | For each room this node belongs to whose `_hs:room-meta` has `public: true` and a |
| 95 | `bridgedTo[bsky]` entry: |
| 96 | |
| 97 | 1. **Roster.** Members with role ≥ peer. Each member is an identity root; its atproto DID is that |
| 98 | identity's **designated credential** (`identity.md`). Here: wj62 = tabertron's identity |
| 99 | (feed `5271cdd4…`), e2jev = ashatron's (feed `13ea8136…`) — confirmed by the operator. |
| 100 | 2. **Skeleton.** Apply `curation` over the members' posts the node holds (favela-peer replicates |
| 101 | followed feeds, so no index is needed), newest first, and emit |
| 102 | `at://<member DID>/app.bsky.feed.post/<tidFor(name)>`. Filter exactly as `feedgen.js:114-135` does |
| 103 | today: `app.bsky.feed.post` only, valid TID rkeys only — one bad rkey poisons the whole response. |
| 104 | 3. **Hand to hpp** with `put-feed-skeleton` (§5.1) for `bridgedTo.did`. |
| 105 | 4. **Labels.** Read `_hs:mod-label` messages (§5.2), role-checked (§3.2), and hand each to hpp as a |
| 106 | `label-intent`. |
| 107 | |
| 108 | ⚠ The deployed favela-bridge (0.0.12) predates its own source: edges (likes, replies, reposts) are |
| 109 | in `lib/project.js` / `plan-edge.js` since `2e8ef98` but were never version-bumped, so no desktop |
| 110 | node projects them. Bump and deploy before adding this pass. |
| 111 | |
| 112 | ### 4.1 Over-length posts: truncate with a link (decided 2026-09-29) |
| 113 | |
| 114 | `design.md` §7 (`:192`) says over-300-grapheme posts are truncated with a permalink; the code |
| 115 | withholds them (`plan.js:174`). **The spec wins.** Truncate at a sentence boundary, append `…`, add |
| 116 | a link facet to the post's page in the public read-only surface (§6). |
| 117 | |
| 118 | Rejected: splitting into a thread. It breaks one record → one at:// URI (parts need derived rkeys |
| 119 | and every edge must pick a part), an edit that changes the part count orphans bsky replies on removed |
| 120 | parts, and markdown does not survive 300-grapheme cuts. ActivityPub has no such limit and keeps the |
| 121 | full text. |
| 122 | |
| 123 | --- |
| 124 | |
| 125 | ## 5. hpp and atproto-bridge |
| 126 | |
| 127 | ### 5.1 Feed skeleton |
| 128 | |
| 129 | New verbs on **`atproto-room/lib/serve-membrane.js`** — the responder atproto-bridge actually |
| 130 | reaches (`device-node.js:637`). ⛔ Not `serve-device.js`; `serve-membrane.js:333-336` warns about |
| 131 | exactly that two-surfaces trap. |
| 132 | |
| 133 | | verb | caller | gate | |
| 134 | |---|---|---| |
| 135 | | `put-feed-skeleton {did, rkey, uris[]}` | favela-bridge, over hpp's control socket | local UDS only | |
| 136 | | `get-feed-skeleton {did, rkey, limit, cursor}` | atproto-bridge | public, like `get-repo-car` | |
| 137 | | `describe-feed-generator {did}` | atproto-bridge | public | |
| 138 | |
| 139 | The skeleton is state in the DID's StateBase, so every co-owner of wqnwul serves the same one. |
| 140 | |
| 141 | ⚠ Most verbs on that channel require `canLogin` (`jwtSecret && appPasswordHash`, |
| 142 | `sessions.js:14`); wqnwul has neither on stortron or tabertron. These must be ungated reads. |
| 143 | |
| 144 | ### 5.2 Labels — `LabelRoom`, wired to hpp's election |
| 145 | |
| 146 | `SOCIAL-MODEL.md` §6.7: a moderation signal is an **edge** `(moderator, flag, target)` with a |
| 147 | reason, written as a message in a **moderation room**; subscribing to a community's signals is |
| 148 | joining that room; `flag` → a signed atproto label, → fediverse **nothing** (`loss[]`). |
| 149 | |
| 150 | - Decision: a `_hs:mod-label` message `{target, val, neg?, reason}` in the community's moderation |
| 151 | room, from a moderator (§3.2). |
| 152 | - Signing: `atproto-room/lib/label-room.js` already does the rest — intents in the DID's StateBase, |
| 153 | one holder signs with the `#atproto_label` key (`label-signer.js`, RFC6979-deterministic so a |
| 154 | handoff never forks), signed labels in the shared `__label__` log, `projectTo(feedCore)` appends |
| 155 | them to a hypercore. |
| 156 | - ⛔ **`LabelRoom` elects its signer with `Lease`** (`label-room.js`). hpp replaced the lease with |
| 157 | a presence-based election after outages (atproto-room skill). Wire `tick()` to hpp's election, not |
| 158 | to `Lease`, or the two elections can disagree about who signs. |
| 159 | - New verb `label-intent {did, uri, val, neg}` (local UDS only) → `LabelRoom.label/negate`. |
| 160 | - ⚠ The label key IS the repo key (`#atproto_label` == `#atproto` in wqnwul's PLC doc), so labels |
| 161 | inherit the key-copied-to-every-co-owner posture that `identity.md` §6.1.1 wants replaced. Not made |
| 162 | worse; not fixed here. |
| 163 | |
| 164 | **Not urgent:** no labels exist today. Build it when a community needs moderation. |
| 165 | |
| 166 | ### 5.3 atproto-bridge: P2P only |
| 167 | |
| 168 | - `getFeedSkeleton`, `describeFeedGenerator`, `queryLabels` → `DidPeer` calls to the §5.1/§5.2 verbs |
| 169 | over the existing `hyper-proto-peer/rpc/v1` channel (`atproto-did-peer/lib/did-peer.js`). |
| 170 | - `subscribeLabels` → **replicate the label hypercore and frame it locally**, the same pattern as |
| 171 | `subscribeRepos` from the firehose core. protomux-rpc is request/response only. |
| 172 | - Delete `FAVELA_WEB_UPSTREAM`, `favela-proxy.js`, and the WS upstream at `server-bare.js:431-444`. |
| 173 | The membrane's did:web doc advertises `#bsky_fg` unconditionally for DIDs that have a feed. |
| 174 | |
| 175 | --- |
| 176 | |
| 177 | ## 6. favela-surface: public read-only mode (decided 2026-09-29) |
| 178 | |
| 179 | A second listener, or a mode flag, that serves **GET only** and renders **only public content**: |
| 180 | |
| 181 | - The node's own feed and the feeds it follows. Feeds are public by key (#61 §8: "unlisted is not |
| 182 | private"), and favela-bridge already projects the own feed publicly. |
| 183 | - Rooms whose `_hs:room-meta` says `public: true` — the community pages (`/r/<key>`). |
| 184 | - `/blob`, `/avatar`, `/static` for the above. |
| 185 | |
| 186 | Never rendered: documents (keyed, private by definition), non-public rooms, DMs, knocks, |
| 187 | notifications, settings, `/api/*`, and every POST. Unauthenticated requests to any of those → 404, |
| 188 | not a login redirect — the public site should not advertise that there is more. |
| 189 | |
| 190 | Each post gets a stable public URL — the permalink §4.1 links to. |
| 191 | |
| 192 | The proxy entry `favela` (`~/.config/7rnx-proxy/registry.json`, stortron:4041) moves to this |
| 193 | listener. favela-surface binds `127.0.0.1` today; the public listener needs `FAVELA_SURFACE_HOST` |
| 194 | reachable by the proxy. |
| 195 | |
| 196 | --- |
| 197 | |
| 198 | ## 7. Migrating loser-club |
| 199 | |
| 200 | loser-club is a favela-community `CommunityRoom` today (`favela-community/lib/community-room.js`), |
| 201 | DID-keyed, on autobase. Nothing links it to favela-peer. The migration: |
| 202 | |
| 203 | | old (favela-community) | new (favela-peer room) | |
| 204 | |---|---| |
| 205 | | `member:<did>` — wj62, e2jev | members = the identity roots behind them (tabertron, ashatron) | |
| 206 | | roles `owner` / `writer` / `mirror` | owner → **admin**; writer → **peer**; mirror → none (a replicator, not a member) | |
| 207 | | `ban`/`unban` | `remove-member` (moderator); a ban list, if kept, is a moderation-room message | |
| 208 | | `set-curation` | `_hs:room-meta.curation` | |
| 209 | | `meta:did` = wqnwul | `_hs:room-meta.bridgedTo[bsky].did` | |
| 210 | |
| 211 | The community DID does not change, so the bsky feed URI |
| 212 | `at://did:plc:wqnwul…/app.bsky.feed.generator/loser-club` survives the move. |
| 213 | |
| 214 | ⚠ Read the live old room's roster, bans and curation **before** migrating — this table is from the |
| 215 | schema, not from the data. |
| 216 | |
| 217 | --- |
| 218 | |
| 219 | ## 8. Order |
| 220 | |
| 221 | Each step needs the one before it. |
| 222 | |
| 223 | 1. favela-bridge version bump — ship the unshipped edge projection. |
| 224 | 2. favela-peer: `_hs:room-meta` public/bridgedTo/curation, and the shared role-check function (§3). |
| 225 | 3. favela-surface public read-only mode (§6). Can run in parallel with 4–5. |
| 226 | 4. favela-bridge room pass (§4) and truncate-with-link (§4.1). |
| 227 | 5. hpp feed verbs (§5.1); atproto-bridge P2P routing (§5.3). |
| 228 | 6. Create the loser-club favela-peer room and migrate it (§7). |
| 229 | 7. **Parity gate:** the new `getFeedSkeleton` for loser-club returns the same URIs as favela-web's |
| 230 | live one, URI for URI, before any switch. |
| 231 | 8. Switch atproto-bridge off `FAVELA_WEB_UPSTREAM`; repoint `favela.7rnx.net`. |
| 232 | 9. Retire: stop + disable autostart for favela-web on stortron, tabertron and ashatron; then |
| 233 | favela-community / favela-node. **Data stays on disk.** |
| 234 | 10. Labels (§5.2) when moderation is needed. |
| 235 | |
| 236 | --- |
| 237 | |
| 238 | ## 9. Open |
| 239 | |
| 240 | 1. **Designation lookup from another node.** Step 4.1 needs "identity root → designated DID" for |
| 241 | *other* members (e2jev from tabertron's bridge). Whether favela-identity answers that for a |
| 242 | non-local identity is unverified. |
| 243 | 2. **Which node runs the room pass.** favela-bridge runs on tabertron and ashatron, not stortron. |
| 244 | hpp co-hosts wqnwul on tabertron (creator) and stortron (joiner). Running the pass on one node is |
| 245 | enough, because the skeleton lands in the shared StateBase. |
| 246 | 3. **Room posts vs member posts.** `curation: roster` reproduces today's feed — everything members |
| 247 | post anywhere. A feed of only what is posted *in* the community room needs room posts projected |
| 248 | into their authors' repos, which is #61 §3 item 8 (gathering N rooms into one signed repo). Out of |
| 249 | scope here. |
| 250 | 4. **Spec/code drift, settled here:** §4.1 (truncate). Also open: `record-vocabulary.md` §3.1 says |
| 251 | the rkey pairing is derivable both ways; `tidFor` is one-way. One of them changes. |