aboutsummaryrefslogtreecommitdiffstats
path: root/devel-phases-next.md
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-10 22:12:59 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-10 22:12:59 +0200
commit60c4570e72e36c2a9720593c8baec74ee2ab52d6 (patch)
treee833e222a6a95e64e5b24a0813882636044198c2 /devel-phases-next.md
parent1a53eb4cc404ec94658fde0ae04cfe2ccf1810dc (diff)
downloadmeshbay-60c4570e72e36c2a9720593c8baec74ee2ab52d6.tar.gz
feat: Phase 9.1–9.5 — WebRTC DataChannel transport for browser P2P
Browser clients can now connect P2P to nodes behind residential NAT via WebRTC DataChannel with ICE/STUN. Validated on SFR Port-Restricted Cone NAT + 4G CGNAT across three scenarios (WiFi LAN, 4G IPv6, 4G IPv4 STUN). No TURN relay needed. Hub serves only as signaling relay (<1 KB). New files: - webrtc_server.py: aiortc-based WebRTC transport (node side) - signaling.py: SDP/ICE relay endpoint (hub side) - transport.js: browser WebRTC client with msgpack framing - webrtc-test.html: spike test page for browser→NAT→node validation - test_webrtc_transport.py: 4 tests (handshake, file transfer, auth, guard) - meshbay-draft-v4.md: architecture spec updated for web client Modified: - hub_client.py: WebRTC offer handling via hub WebSocket - revocation.py: node_id from WS auth + webrtc_answer routing - pyproject.toml: aiortc>=1.9 dependency 123 tests passing (117 existing + 6 new). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'devel-phases-next.md')
-rw-r--r--devel-phases-next.md419
1 files changed, 270 insertions, 149 deletions
diff --git a/devel-phases-next.md b/devel-phases-next.md
index f4793df..09d07fa 100644
--- a/devel-phases-next.md
+++ b/devel-phases-next.md
@@ -1,209 +1,330 @@
# MeshBay — Next Implementation Phases
-> Base: Phases 1-6 complete. demo-v2 NAT QUIC validated.
-> Architecture reference: docs/meshbay-draft-v3.md
+> Base: Phases 1–8 complete. 117 tests. demo-v3 validated against meshbay.org.
+> Architecture reference: docs/meshbay-draft-v4.md
> First security review: first-review.md (2026-08-10)
---
-## Phase 7 — Node v2 : production, streaming, chat
+## Phase 7 — Node v2 : production, streaming, chat ✅ DONE
-**Objective:** a node usable for daily operations — multi-group, smooth streaming,
-integrated chat, fast reconnection.
+Commit: fc56585 — 26 files, +2155/−159 lines, 109 tests.
-### Prerequisites (from first security review, 2026-08-10)
+| # | Component | Status |
+|---|---|---|
+| 7.0 | JWT group claims + node authz check | ✅ |
+| 7.1 | QUIC 0-RTT session resumption | ✅ |
+| 7.2 | Signaling `client_incoming`/`punch_ready` + jti denylist push | ✅ |
+| 7.3 | Multi-group daemon (1-port multiplexing) | ✅ |
+| 7.4 | HLS streaming via QUIC | ✅ |
+| 7.5 | Chat: Sender Keys protocol + storage + MNP wire | ✅ |
+| 7.6 | Chat: local web UI + WS push to members | ✅ |
+
+---
+
+## Phase 8 — Hub v2: admin, federation, security ✅ DONE
+
+Commit: 46918ec — 20 files, +508/−90 lines, 117 tests.
+Deployed to meshbay.org. Existing emails encrypted. DB schema migrated.
+
+| # | Component | Status |
+|---|---|---|
+| 8.1 | Admin roles — config-based `require_admin` | ✅ S1 resolved |
+| 8.2 | Email encrypted at rest — AES-256-GCM, HKDF | ✅ S2 resolved |
+| 8.3 | Refresh token rotation — family-based reuse detection | ✅ S5 resolved |
+| 8.4 | Federation DB persistence (HubPeer model) | ✅ |
+| 8.5 | Federation token verification async (DB-backed) | ✅ |
+| 8.6 | CSAM hash check in swarm registration | ✅ |
+| 8.7 | Rate limiting on auth endpoints (5/10/20 per min) | ✅ |
+| 8.8 | Healthcheck endpoint (GET /v1/health) | ✅ |
+| 8.9 | IP log cleanup background task (365-day retention) | ✅ |
+| 8.10 | Argon2id bumped to 256 MB (pw_version=2, rehash on login) | ✅ |
+
+---
+
+## Phase 9 — Web client: WebRTC transport + core SPA
+
+**Objective:** a web browser can connect P2P to a node behind residential NAT,
+browse files, download, stream video, and chat — with zero data through the hub.
+
+**Architecture decisions (settled 2026-08-10):**
+
+### Transport: WebRTC DataChannel for browsers
-Before writing Phase 7 production code, two critical design gaps must be
-addressed — see `first-review.md` for full analysis:
+Native clients (desktop, Android) use QUIC with `punch_nat()` — already validated
+in demo-v2 on SFR residential (Port-Restricted Cone NAT).
-1. **[C2] JWT must carry group membership claims.** Add `"groups": [group_ids]`
- to `issue_access_token()`. Node MNP handshake must verify the requested
- group_id is in the JWT's groups claim before serving any content. Without
- this, any authenticated user can access any group on the node.
- → Implement in 7.3 (multi-group daemon) before any other milestone.
+Browsers cannot use QUIC for NAT traversal because WebTransport does not allow
+the browser to choose its UDP source port. Port-Restricted Cone NAT requires the
+client to connect from the exact port the node probed — impossible for browsers.
-2. **[C1] Chat encryption: Sender Keys, not shared Double Ratchet.** The Double
- Ratchet is a pairwise (1:1) protocol — sharing a single ratchet state across
- N group members causes key/nonce reuse (AEAD catastrophic failure). The
- architecture now uses **Sender Keys** (Signal Groups approach): each member
- has their own sending chain key, O(N) state.
- → Implement in 7.5. The existing `ratchet.py` is kept for future 1:1 DM.
+**Solution:** WebRTC DataChannel with ICE/STUN. The browser's built-in WebRTC
+stack handles NAT traversal automatically. The node uses `aiortc` (same author as
+`aioquic`, already referenced in draft-v3 as [future]).
-### Architectural decisions (settled)
+ICE is strictly superior to our custom `punch_nat()` for this use case:
+- Both sides send STUN binding requests simultaneously → mutual hole-punching
+- No need for the client to pre-announce its port
+- Handles both sides behind NAT
+- Battle-tested by billions of users (Google Meet, Discord, etc.)
-**Multi-group → multiplexing on a single QUIC port**
-A node exposes a single QUIC port (e.g. 19010). All hosted groups share this
-port. The group is identified in the MNP handshake by the `group_id` in the
-JWT. Advantages: one NAT hole to maintain, one manual port forward if needed.
-The QUIC server routes each connection to the appropriate IndexGroup/GEK
-after JWT verification **and group membership authorization check**.
+The MNP protocol (handshake, file_request, file_chunk, chat_message, etc.) runs
+identically over WebRTC DataChannel as over QUIC streams. Same E2E encryption.
+
+**Node dual transport:**
+- QUIC (port 19000) — native clients, already in place
+- WebRTC DataChannel — browsers, using `aiortc`
+
+### Signaling: hub WebSocket relay
+
+The hub relays WebRTC signaling (SDP offer/answer, ICE candidates) between
+browser and node. This is the same role described in draft-v3 section 4.1.3:
+"NAT traversal coordination [...] stateless [...] <1 KB per message."
-**Signaling punch/connect (via hub WebSocket)**
-Currently the node punches blindly at startup → 12.7s handshake (NAT hole ages
-before the client arrives). Solution:
```
-Client → Hub (HTTPS) : "I'm about to connect to node X, I'm at IP:PORT"
-Hub → Node (WS) : message "client_incoming: {peer_ip, peer_port}"
-Node → NAT (UDP) : punch_nat(peer_ip, peer_port) immediately
-Node → Hub (WS) : "punch_ready"
-Hub → Client (HTTPS) : "connect now"
-Client → Node (QUIC) : < 2s after probe → fresh NAT entry → < 200ms
+Browser → Hub (HTTPS) : POST /v1/nodes/{id}/webrtc/offer {sdp, ice_candidates}
+Hub → Node (WS) : {type: "webrtc_offer", sdp, ice_candidates, peer_id}
+Node → Hub (WS) : {type: "webrtc_answer", sdp, ice_candidates, peer_id}
+Hub → Browser (SSE) : {sdp, ice_candidates}
```
-The hub→node WebSocket channel already exists (`hub/api/revocation.py`).
-Just add `client_incoming` / `punch_ready` message types.
-This mechanism is a simplified ICE (Interactive Connectivity Establishment).
-The same WebSocket channel also carries **jti denylist push** (security review
-S3): when the hub revokes a token, it pushes the jti to all connected nodes.
-Nodes maintain an in-memory set and check it during MNP handshake.
+After signaling, the DataChannel is P2P. Hub is no longer involved.
-**Chat — between a forum and Signal**
-Not a real-time ephemeral chat (Signal) nor a heavy forum.
-Model: **E2E encrypted discussion thread, persisted on the node**.
-- Short messages + attachments (like Signal group)
-- Optional threads/topics for structure (like a light forum)
-- History stored on the node (not ephemeral)
-- Push for connected members, pull for offline
-- **Sender Keys** protocol for encryption (security review C1 — Double Ratchet
- is pairwise only, cannot be shared across group members)
-- Scope: per group (not per user pair)
-- No automatic deletion (group admin manages retention)
+### UI: Preact SPA
-### Milestones
+- **Framework:** Preact (~3 KB gzipped) + preact-router
+- **Build:** esbuild (single binary, no node_modules bloat) for minification
+- **Theming:** CSS `prefers-color-scheme` + localStorage toggle (dark/light)
+- **i18n:** JSON translation files loaded client-side, English default
+- **Responsive:** sidebar collapses to hamburger on mobile viewports
+- **Crypto:** existing `crypto.js` (SubtleCrypto AES-GCM) for E2E decryption
-| # | Component | File(s) | Priority |
-|---|---|---|---|
-| 7.0 | JWT group claims + node authz check | `hub/auth.py` + `node/transport/quic_server.py` | **Blocker** |
-| 7.1 | QUIC 0-RTT session resumption | `transport/quic_server.py` + `quic_client.py` | High |
-| 7.2 | Signaling `client_incoming`/`punch_ready` + jti denylist push | `hub/api/revocation.py` + `node/hub_client.py` | High |
-| 7.3 | Multi-group daemon (1-port multiplexing) | `node/daemon.py` — N IndexGroups, 1 QuicChunkServer | High |
-| 7.4 | HLS streaming via QUIC | `node/transport/hls.py` — segments as QUIC streams | Medium |
-| 7.5 | Chat: Sender Keys protocol + storage + MNP wire | `common/senderkeys.py` + `node/chat/store.py` | Medium |
-| 7.6 | Chat: local web UI + WS push to members | `node/ui/app.py` WebSocket for notifications | Medium |
-| 7.7 | Argon2id calibration CLI | `node/daemon.py` — `meshbay-node calibrate-argon2` | Low |
+### Hub role (reminder — fundamental constraint)
-**Remaining open questions:**
-- Do groups on the same node share the node's Ed25519 key? (likely yes)
-- Multi-group UI at localhost:18000: tabs per group or unified list?
+The hub is a registrar and signaling facilitator. It stores ONLY:
+- User accounts (login, encrypted email, public keys, keypair bundle)
+- Group metadata (name, admin, members, GEK bundles — no file indexes)
+- Node registrations (endpoint hints, public keys)
----
+All data (files, streams, chat messages, directory indexes) lives on mesh nodes.
+Clients (web or native) transfer data E2E with nodes. The hub never touches
+content. This is non-negotiable.
-## Phase 8 — Hub v2: admin, federation, production security
+### Chat/forum storage
-**Objective:** hub ready for public operation — admin roles, MHP network,
-CSAM integrated, monitoring.
+Chat messages are stored on the node(s) hosting the group, not on the hub.
+The browser retrieves chat history from the node via DataChannel, same as files.
+If no node in the group is online, the group (including chat) is unavailable.
+This is inherent to the P2P model and acceptable.
-| # | Component | File(s) | Priority |
+### File search
+
+Content is not indexed on the hub. Search works client-side:
+- Node provides a Mesh Group Index (file metadata: names, paths, sizes, hashes)
+- For private groups, the index is GEK-encrypted — hub stores it opaque, client decrypts
+- Browser caches decrypted indexes in IndexedDB (~50–100 MB quota, extensible)
+- Search runs locally on cached indexes — instant, no network call, no hub involvement
+
+### Milestones
+
+| # | Component | Files | Priority |
|---|---|---|---|
-| 8.1 | Admin roles (hub_admin flag on User) + authz on revocation | `hub/db/models.py` + `hub/api/admin.py` + `hub/api/revocation.py` | **High — S1** |
-| 8.2 | Email encryption at rest | `hub/db/models.py` — AES-256-GCM with config-derived key | **High — S2** |
-| 8.3 | Refresh token rotation (one-time use) | `hub/api/users.py` — rotate on each use, detect reuse | **High — S5** |
-| 8.4 | MHP inter-hub network (not just in-memory) | `hub/api/federation.py` + Alembic migration | High |
-| 8.5 | federated_groups DB persistence | `hub/db/models.py` FederatedGroup already defined | High |
-| 8.6 | CSAM real DB (import NCMEC/IWF) | `hub/csam.py` — import CLI + API update | High |
-| 8.7 | Rate limiting on all authenticated endpoints | `hub/api/middleware.py` — extend slowapi | Medium — M3 |
-| 8.8 | Metrics / healthcheck | `hub/api/health.py` | Medium |
-| 8.9 | Cleanup IP logs (purge > 1 year) | `hub/tasks/cleanup.py` — APScheduler | Medium |
-| 8.10 | Alembic migration Argon2id params | Bump migration + `hub/auth.py` | Low |
+| 9.1 | **Spike: WebRTC DataChannel on node** | `aiortc` integration, 4 tests (handshake, file transfer, auth, guard) | ✅ |
+| 9.2 | WebRTC signaling endpoints on hub | `hub/api/signaling.py` — relay SDP/ICE, 2 tests | ✅ |
+| 9.3 | WebRTC→MNP transport adapter on node | `node/transport/webrtc_server.py` + hub_client WebRTC handler | ✅ |
+| 9.4 | `transport.js` — browser WebRTC client | `static/transport.js` — connect, handshake, fetch, msgpack | ✅ |
+| 9.5 | **Spike: E2E browser→NAT→node file transfer** | Mobile 4G → SFR NAT → node, IPv4 STUN + IPv6 validated | ✅ |
+| 9.6 | Preact SPA shell (login, routing, theme) | `static/app.js`, `static/css/style.css` | High |
+| 9.7 | Group list + file explorer UI | `static/components/GroupList.js`, `FileExplorer.js` | High |
+| 9.8 | File download via DataChannel | `static/components/Download.js` — chunk reassembly | High |
+| 9.9 | Video streaming via DataChannel | HLS segments → MediaSource API | Medium |
+| 9.10 | Chat/forum UI via DataChannel | `static/components/ChatView.js` — Sender Keys | Medium |
+| 9.11 | i18n framework + English strings | `static/i18n/en.json` | Medium |
+| 9.12 | Settings UI (profile, theme, notifications) | `static/components/Settings.js` | Medium |
+| 9.13 | Tests: unit + integration | WebRTC transport, MNP over DataChannel | High |
+
+**Critical path validated (2026-08-10):** 9.1 → 9.5 all pass. WebRTC DataChannel
+works browser → node through SFR residential NAT, confirmed with three scenarios:
+
+| Test | ICE path | Result |
+|---|---|---|
+| WiFi LAN (same network) | IPv6 direct | OK, ~100ms |
+| Mobile 4G SFR + IPv6 | IPv6 inter-network | OK, ~600ms |
+| Mobile 4G SFR + IPv4 only | STUN hole-punch IPv4 | OK, ~650ms |
-Items 8.1-8.3 are from the first security review (S1, S2, S5).
+Node behind SFR Port-Restricted Cone NAT + mobile behind SFR CGNAT 4G.
+No TURN relay needed. ICE/STUN handles both NAT types automatically.
-**Questions to clarify:**
-- Who can be hub_admin? First registered user? Config toml?
-- MHP: inter-hub authentication via JWT or mutual TLS?
+**Dependencies added:**
+- `aiortc>=1.9` in `meshbay-node/pyproject.toml` ✅
+- `esbuild` as a dev tool (single binary, not npm) — needed for 9.6+
+- `preact` + `preact-router` (ESM imports, no npm needed — CDN or vendored)
---
-## Phase 9 — Android client MVP
+## Phase 10 — meshbay.org site + admin/moderation UI
-**Objectif :** app Android permettant de créer un compte, rejoindre un groupe,
-télécharger des fichiers depuis un node.
+**Objective:** meshbay.org becomes both a production hub and the project's public
+website, with admin/moderation interfaces and user-facing features.
-**Stack technique à décider :**
-- **Kotlin natif** : plus de contrôle, accès direct aux APIs Android (WebRTC, QUIC via fork)
-- **Flutter** : cross-platform (iOS futur), Dart, mais bindings aioquic inexistants
-- **React Native** : JS, même problème de bindings natifs QUIC
+### Site architecture
-**Recommandation :** Kotlin natif. La partie critique (QUIC/UDP + crypto) est en C/Rust via
-des bindings JNI. La couche UI peut être Jetpack Compose.
+Two layers, cleanly separated:
+- **Generic hub** (API + web app) — reusable by any hub operator
+- **Site overlay** — meshbay.org-specific pages (landing, /downloads, /about)
-| # | Composant | Tech | Priorité |
-|---|---|---|---|
-| 9.1 | Hub client (auth, groups, GEK) | Kotlin + Retrofit | Haute |
-| 9.2 | Crypto (Ed25519, X25519, ChaCha20) | Bouncy Castle JVM | Haute |
-| 9.3 | QUIC client | quiche (Cloudflare, Rust JNI) ou QUIC4J | Haute |
-| 9.4 | NAT traversal (STUN + punch) | Kotlin native UDP | Haute |
-| 9.5 | File browser + download | Kotlin + streaming IO | Haute |
-| 9.6 | Chat UI | Jetpack Compose | Moyenne |
-| 9.7 | Node UI pairing (QR code) | Android camera + hub API | Moyenne |
+The site overlay is served by Caddy (static files) with priority over the hub.
+The hub serves the SPA for authenticated users at `/app/`.
-**Préalable à clarifier :** quels bindings QUIC existent sur Android ?
-`quiche` de Cloudflare (en Rust, JNI) est le plus mature.
+```
+site/ # meshbay.org-specific (not in generic hub package)
+├── index.html # Landing page — project promotion
+├── downloads.html # Package repos (Ubuntu, Fedora, Android APK)
+├── about.html # Project info, GitHub link, contact
+└── assets/ # Landing page CSS/images
+```
----
+### User roles
-## Phase 10 — Web client v2 : groupes privés + streaming
+| Role | Capabilities |
+|---|---|
+| `user` | Standard user — browse, download, chat, manage own profile |
+| `moderator` | Review reports, suspend content/groups/users |
+| `admin` | All moderator rights + hub management, user management, logs |
-**Objectif :** navigateur peut décoder le contenu privé (AES-GCM) et streamer des vidéos.
+Role stored on User model. `require_moderator` dependency (checks role ≥ moderator).
+`require_admin` already exists (Phase 8.1 — config-based, extended to DB role).
-| # | Composant | Fichier(s) | Priorité |
-|---|---|---|---|
-| 10.1 | Web client : décryptage privé (AES-GCM + SubtleCrypto) | `static/crypto.js` MeshBayCrypto | Haute |
-| 10.2 | Web client : groupe-type "browser" (AES-GCM GEK) | Hub : `cipher` field sur Group | Haute |
-| 10.3 | Player HLS dans browser (hls.js + déchiffrement) | `static/app.js` + hls.js | Haute |
-| 10.4 | Chat browser (Sender Keys JS — AES-GCM via SubtleCrypto) | `static/senderkeys.js` | Moyenne |
-| 10.5 | PWA / Service Worker | offline + cache | Basse |
+### Milestones
-**Question clé :** pour le streaming privé en browser, deux approches :
-- **AES-GCM GEK** (actuel) : browser-native mais nécessite un groupe dédié
-- **ChaCha20 via WASM** : même GEK que les clients natifs, plus complexe
+| # | Component | Priority |
+|---|---|---|
+| 10.1 | Landing page + /downloads + /about | High |
+| 10.2 | Moderator role + `require_moderator` dependency | High |
+| 10.3 | Moderation UI (report list, suspend content/group/user) | High |
+| 10.4 | Admin UI (hub management, user list, logs viewer) | High |
+| 10.5 | Notification system (invitations, new content, maintenance) | Medium |
+| 10.6 | User settings (profile, per-group options, privacy, mute) | Medium |
+| 10.7 | Public group search (name + keyword in description) | Medium |
+| 10.8 | Front page (notifications feed, prioritized: contacts → private → public) | Medium |
+| 10.9 | Package repositories (APT for Ubuntu, DNF for Fedora) | Medium |
+| 10.10 | Auto-update check endpoint for clients | Low |
+
+### Hub mirror (design only — implementation deferred)
+
+A mirror hub is a complete replica of the primary hub (same user DB, same groups,
+same GEK bundles, same storage). Purpose: load distribution via DNS round-robin.
+
+**Design constraints:**
+- Shared Ed25519 private key (transferred once at setup, securely)
+- PostgreSQL logical replication for active-active read/write on both mirrors
+- Both mirrors can issue JWTs (same signing key)
+- DNS round-robin (2+ A records on meshbay.org)
+- If one mirror goes down, the other continues serving
+
+**Not implemented now.** The design must not prevent future implementation:
+- Hub config and private key paths must be externalizable
+- No hub-specific state that can't be replicated
+- JWT verification must not depend on hub-local state
---
-## Phase 11 — Résilience réseau : TURN relay, 0-RTT, CGNAT
+## Phase 11 — Android client MVP
+
+**Objective:** Android app for account creation, group browsing, file download,
+chat. No node functionality on mobile (client-only).
-**Objectif :** fonctionner même derrière les NAT les plus restrictifs (mobile 4G/5G CGNAT).
+**Stack:** Kotlin native + Jetpack Compose. QUIC via `quiche` (Cloudflare, Rust
+JNI binding). Crypto via Bouncy Castle JVM. Same NAT traversal as desktop native
+clients (`punch_nat` + QUIC).
-| # | Composant | Notes | Priorité |
+| # | Component | Tech | Priority |
|---|---|---|---|
-| 11.1 | Mesh Relay TURN server | Node Python serveur UDP relay chiffré | Haute |
-| 11.2 | Relay registration MHP | Hub : `/v1/relays/` + annonce aux nodes | Haute |
-| 11.3 | Node : fallback automatique → relay | Après échec STUN dans discover_nat() | Haute |
-| 11.4 | Punch coordination signaling | Hub WS → node punch → client connect < 2s | Haute |
-| 11.5 | QUIC 0-RTT (aioquic session tickets) | Node stocke ticket → reconnexion < 50ms | Moyenne |
-| 11.6 | Test CGNAT mobile 4G | Spike dédié : node mobile → node fixe | Moyenne |
-| 11.7 | Connection pool (1 QUIC conn = N requêtes) | Node : réutilisation de stream par user | Moyenne |
+| 11.1 | Hub client (auth, groups, GEK) | Kotlin + Retrofit | High |
+| 11.2 | Crypto (Ed25519, X25519, ChaCha20) | Bouncy Castle JVM | High |
+| 11.3 | QUIC client | quiche (Rust JNI) | High |
+| 11.4 | NAT traversal (STUN + punch) | Kotlin native UDP | High |
+| 11.5 | File browser + download | Kotlin + streaming IO | High |
+| 11.6 | Chat UI | Jetpack Compose | Medium |
+| 11.7 | Contact list integration | Android Contacts API (permission-gated) | Medium |
+| 11.8 | Account creation from app | Registration flow + keypair bundle | High |
+
+**Cross-device compatibility:** the user may switch between web and Android.
+The `keypair_bundle` (encrypted, stored on hub) enables this — same credentials,
+same keys on both platforms. Notification state and read markers should sync
+via hub (small encrypted blob per user, minimal storage).
+
+**Upload from mobile:** posting photos/videos to a group. The mobile uploads to
+the group's node(s), not to the hub. The node stores it. MNP protocol extended
+with an `upload` message type for client→node push.
+
+**Out of scope:** node functionality on mobile, Mac/iPhone support.
---
-## Phase 12 — RPM/DEB packaging production + CI
+## Phase 12 — Network resilience (optional, low priority)
-**Objectif :** packages installables, CI qui tourne les tests, releases signées.
+**Objective:** handle edge cases — symmetric NAT (CGNAT mobile), TURN relay,
+0-RTT reconnection. Not needed for typical residential users.
-| # | Composant | Notes |
+| # | Component | Priority |
|---|---|---|
-| 12.1 | RPM build pipeline (Fedora, RHEL) | rpmbuild + spec files déjà écrits |
-| 12.2 | DEB build pipeline (Ubuntu, Debian) | dpkg-deb + control déjà écrits |
-| 12.3 | GitHub Actions CI | pytest + ruff sur PR |
-| 12.4 | Release signing | GPG key pour les packages |
-| 12.5 | Repo apt/dnf auto-hébergé | meshbay.org/packages/ |
+| 12.1 | Mesh Relay TURN server | Low |
+| 12.2 | Relay registration via MHP | Low |
+| 12.3 | Node fallback to relay after ICE failure | Low |
+| 12.4 | QUIC 0-RTT (session tickets) | Medium |
+| 12.5 | Connection pool (1 QUIC conn = N requests) | Medium |
+| 12.6 | Test CGNAT mobile 4G | Low |
+
+**Note:** enterprise users behind restrictive firewalls can configure port
+forwarding themselves. This phase targets the ~15% of residential connections
+where even ICE/STUN fails (symmetric NAT behind CGNAT). Not a priority —
+the user explicitly deprioritized this.
+
+---
+
+## Phase 13 — RPM/DEB packaging + CI
+
+| # | Component |
+|---|---|
+| 13.1 | RPM build pipeline (Fedora, RHEL) |
+| 13.2 | DEB build pipeline (Ubuntu, Debian) |
+| 13.3 | GitHub Actions CI (pytest + ruff on PR) |
+| 13.4 | Release signing (GPG key) |
+| 13.5 | Repo apt/dnf on meshbay.org/packages/ |
+| 13.6 | Android APK distribution on meshbay.org/downloads/ |
---
-## Ordre recommandé
+## Recommended order
```
-Phase 7 (Node v2) ← débloque l'usage réel au quotidien
-Phase 8 (Hub v2) ← stabilisation, admin, CSAM
-Phase 11 (Relay+0-RTT)← résout le handshake 12.7s et CGNAT mobile
-Phase 9 (Android) ← client mobile, long chantier
-Phase 10 (Web v2) ← streaming privé browser
-Phase 12 (Packaging) ← distribution
+Phase 9 (Web client) ← core product: browser P2P to nodes
+Phase 10 (Site + admin UI) ← public-facing site, moderation, admin
+Phase 11 (Android) ← mobile client, long effort
+Phase 13 (Packaging) ← distribution
+Phase 12 (Resilience) ← optional, edge cases only
```
-**Next structural decisions (all resolved — see first-review.md):**
-1. Multi-group on a single QUIC port ✅ (decided Phase 7)
-2. Signaling punch/connect via existing hub WS ✅ (decided Phase 7)
-3. Chat is a core feature, not a module ✅ (decided draft v3)
-4. Chat encryption: Sender Keys ✅ (decided in security review)
-5. JWT group claims required ✅ (decided in security review)
+Phase 9 is the critical path. Milestone 9.5 (spike: browser → NAT → node file
+transfer) is the single most important validation in the project. If it works,
+the web client is viable. If not, the architecture needs fundamental rethinking.
+
+---
+
+## Structural decisions (all resolved)
+
+1. Multi-group on a single QUIC port ✅ (Phase 7)
+2. Signaling punch/connect via hub WS ✅ (Phase 7)
+3. Chat is a core feature, not a module ✅ (draft v3)
+4. Chat encryption: Sender Keys ✅ (security review)
+5. JWT group claims required ✅ (security review)
+6. Admin model: config-based ✅ (Phase 8)
+7. Refresh token rotation: family-based ✅ (Phase 8)
+8. Email encrypted at rest: AES-256-GCM ✅ (Phase 8)
+9. Argon2id params: 256 MB, pw_version for migration ✅ (Phase 8)
+10. **Browser transport: WebRTC DataChannel + ICE/STUN** ✅ (decided 2026-08-10)
+11. **Hub role: registrar + signaling ONLY, never in data path** ✅ (reinforced 2026-08-10)
+12. **Chat stored on nodes, not hub** ✅ (decided 2026-08-10)
+13. **Web UI: Preact SPA, dark/light, responsive, i18n** ✅ (decided 2026-08-10)
+14. **Site overlay: meshbay.org-specific pages separate from generic hub** ✅ (decided 2026-08-10)