itsgoin/IMPLEMENTATION_PLAN_0.6.md
Scott Reimers 921a0ec40a Implementation plan: drop cross-version compat, beta/stable as separate networks
Explicit stance: 0.6.x beta does not interoperate with 0.5 stable.
Removes dual-write migration machinery, mixed-version wire handlers,
and cross-track testing. Users cross tracks via export/import bundle.

Preserves: local upgrade path for user's own data; serde-default wire
field additions; identity bundle format compat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 18:26:55 -04:00

219 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Implementation Plan: Identity Architecture Rollout (0.6.x beta cycle)
## Context
0.5.3 is stable. The 0.6.x beta line introduces the Network-ID / Posting-ID split, multi-persona support, ephemeral rotating DM identities, file-holder CDN restructure, and CDN-only DM privacy.
Full architectural plan: `/home/sologretto/.claude/plans/woolly-nibbling-glade.md`
Canonical reference: `website/design.html` §28
Memory summary: `reference_identity_architecture.md`
### Backward-compatibility stance
**Beta and stable are separate networks.** A v0.6 node is not expected to interoperate with a v0.5 node. This is an explicit decision to reduce implementation overhead — no dual-writing legacy tables, no "match on either old or new field" handlers, no migration-through-mixed-network testing.
What we DO preserve:
- **Local upgrade path.** A user upgrading their own install from 0.5 → 0.6 must not lose data. Existing posts, follows, keys, blobs continue to work. Migration is one-way (users don't downgrade) but must be data-safe.
- **Free wire-level compat.** If a protocol field is already `#[serde(default, skip_serializing_if = "Option::is_none")]`, new fields simply follow that pattern. Old serialized data with missing new fields deserializes fine. No special effort.
- **User data interop across versions.** An exported 0.5 identity bundle must be importable into 0.6.
What we DROP:
- Dual-writing deprecated tables during a phase transition. When a phase swaps out a data structure, the old one is gone.
- "Works with v0.5 peers" testing. Beta is tested against beta.
- Legacy wire-protocol tolerances beyond what serde defaults give for free.
Each phase is a standalone release. Phases are ordered to satisfy internal code dependencies, not wire-compat boundaries. After all phases ship, 0.7.0-beta consolidates and 0.7.x becomes the candidate for the next stable promotion.
---
## Phase 1 (v0.6.0-beta): Remove direct `PostPush` for encrypted posts
**Goal:** Eliminate the sender→recipient traffic signal. Encrypted DMs propagate via the existing ManifestPush / CDN tree, indistinguishable on the wire from any other encrypted post.
**Scope:**
- Remove `push_post_to_recipients` from `crates/core/src/node.rs` entirely
- Remove the `PostPush` message handler from connection.rs and/or leave it as a stub that logs + ignores (avoid needing to invent a new message type ordering)
- Ensure new encrypted posts still trigger a normal header update on neighbor posts (existing CDN logic already does this) so they propagate
- Verify the existing ManifestPush fan-out path reaches recipients who follow the author's posting ID
- Add a "CDN delivery SLA" doc note: expect ~seconds to tens-of-seconds latency for DMs to followers; minutes worst case for offline-then-online recipients
**Verification:**
- Send an encrypted DM with two test devices. Confirm the recipient receives it without any direct-push message firing (network log inspection)
- Measure p50/p95 delivery latency on a small mesh of 0.6.0 nodes
- Confirm cold-contact DMs (non-follower) don't reach — expected until Phase 3 or comment-intro UX arrives
**Risks:**
- Non-follower DMs don't reach until later phases. Ship with UX messaging: "DMs to new contacts require either following them or commenting on their post first."
---
## Phase 2 (v0.6.1-beta): File-holder CDN + header-diff propagation
**Goal:** Replace the upstream/downstream tree with per-file flat holder sets. Prerequisite for multi-device (because upstream-toward-author doesn't work with multiple authoring network IDs).
**Scope:**
- New `file_holders` table: `(file_id, peer_id, last_interaction_ms, direction)` — direction records whether we sent or received the file with this peer, for potential reuse
- Populate on ManifestPush receive, blob fetch, blob serve, engagement diff exchange
- Cap per file_id at 5 holders (LRU on `last_interaction_ms`)
- Refactor engagement diff delivery: instead of "send to upstream," send to the file's up-to-5 known holders
- **Drop `post_upstream` and `post_downstream` tables in this phase's migration.** No dual-write. (Local upgrade safety: before-drop, scan these tables to seed the new `file_holders` table with any existing peer relationships so we don't start empty.)
- ManifestPush propagation logic: when header of file A changes, look up A's holders, send diff to them
- Receiver behavior: apply header diff, pull any referenced new files
**Wire protocol:**
- `BlobHeaderDiff` and `ManifestPush` messages stay the same on the wire
- What changes is the sender's list of destinations (was: upstreams; now: holders)
**Local migration (user upgrading 0.6.0 → 0.6.1):**
- On first startup with new schema, read `post_upstream` + `post_downstream`, populate `file_holders` with those peers as initial holders for their respective posts
- Drop the old tables after the seed migration
- User's existing posts, follows, blobs all untouched
**Verification:**
- Mesh test: create a post, track its propagation; confirm holders accumulate up to 5 diverse peers
- Engagement propagation: a reaction on a deeply-nested post still reaches the author (now: via the post's holders)
- Migration test: upgrade a populated 0.6.0 DB to 0.6.1; confirm existing posts still have at least one holder in `file_holders`
**Risks:**
- Churn in holder sets during network instability (holders going offline trigger LRU replacement). Need soak testing.
- Potential duplicate diff delivery if holder sets overlap. Idempotent application is already required by existing code.
---
## Phase 3 (v0.6.2-beta): Merged pull + recipient-match
**Goal:** Nodes can find DMs addressed to them without a distinguishable "searching for DMs" traffic pattern. Handles the case where a DM exists on a peer you're connected to but you don't follow the author.
**Scope:**
- Add index on `wrapped_key.recipient` in storage (migration)
- Extend `PullSyncRequestPayload` handling: peer returns posts matching `author ∈ query_ids` OR `wrapped_key.recipient ∈ query_ids`
- Client: always include own NodeId in the pull query's NodeId list
- No new message type; existing PullSync handler gets smarter
- UX: no user-visible change (pull is internal)
**Wire protocol:**
- No wire format change. Same fields, broader server-side matching logic.
**Verification:**
- Send a DM from A to B where A is not followed by B. Verify B receives it on next pull cycle (via the recipient-match).
- Benchmark pull query cost with the new OR-clause. Should be near-zero with the index.
**Risks:**
- Query cost grows with posts held × recipient list length. Index is mandatory.
- Peer can keep a log of "query included NodeId X" — tied to your connection anyway, not new info.
---
## Phase 4 (v0.6.3-beta): Posting-key / network-key split
**Goal:** Decouple signing identity from network identity. Foundation for multi-device and multi-persona. This phase ships WITHOUT UI for creating multiple personas — it's the plumbing.
**Scope:**
- `PostingIdentity` struct in `crates/core/src/types.rs`: `{ node_id, secret_seed, display_name, created_at }`
- Storage: `posting_identities` table (list of all held posting keys), `active_default_posting_id` setting
- Storage: migrate existing identity → single posting identity with the same key as the network key (no behavior change for existing users)
- `crypto.rs`: separate signing primitives — `sign_with_posting_key` vs `sign_with_network_key`. Keep existing `sign_manifest` working by delegating to posting-key variant when available
- `Node`: load posting keys alongside network key at startup
- `BlobHeader.author`: populated from posting key (was: network key, but they were equal)
- Posts signed with posting key; connections still use network key
- Export/import bundle includes posting key (in addition to network key)
- Wire: no new message types; `InitialExchange` doesn't need to change because posting IDs are only relevant for signed content, not connection setup
**Wire protocol:**
- `BlobHeader` already has an `author` field. We just populate it from posting key instead of network key.
- First-run migration for upgrading users: existing identity becomes the first posting identity. `posting_id == network_id` for pre-split content. Users can create additional posting identities after the upgrade.
**Verification:**
- Existing identity still works; no data loss after 0.5→0.6 migration chain
- Posts authored before the upgrade still render and validate
- New posts use posting key; confirm signatures verify with the posting key
**Risks:**
- Signature verification regression if `author` field handling changes subtly. Needs unit tests for both "legacy author = network key" and "posting key ≠ network key" cases.
- Storage migration needs transaction safety.
---
## Phase 5 (v0.6.4-beta): Multi-persona UX
**Goal:** Let users create and use multiple posting identities with clean UX.
**Scope:**
- IPC: `list_posting_identities`, `create_posting_identity(name, avatar)`, `set_default_posting_identity(id)`, `delete_posting_identity(id)`
- Frontend: Settings > Personas page with create/list/delete
- Compose box: persona picker (avatar + name + dropdown)
- Contextual defaults: posting to a circle uses that circle's last-used persona
- Feed: merged view, filter pills per persona
- Reply/comment: default persona = whichever decrypted the post
- Subtle per-post labels showing which persona's follow surfaced each item
**Wire protocol:**
- No changes (posting keys are already supported from Phase 4)
**Verification:**
- Create three personas, post from each, confirm peers see three distinct authors
- DM each persona from a peer; confirm messages route to separate inbox threads locally
- Social graph separation test: follow one peer from Persona A and a different peer from Persona B; confirm merged feed shows both but filter isolates each
**Risks:**
- UX complexity regression — the merged feed with filters is non-trivial. Start with two-persona users and expand.
- Existing users with single identity should see ZERO UI change until they opt in to creating a second persona.
---
## Phase 6 (v0.6.5-beta): Ephemeral rotating DM IDs + local archive
**Goal:** Maximum traffic-graph concealment for DMs. Each thread gets a rotating posting ID, messages include handshake for the next ID, local archive preserves history.
**Scope:**
- Per-thread ephemeral posting ID generation
- Handshake field in encrypted post payload: `next_posting_id: SecretSeed`
- Sliding window of last 10 accepted IDs per thread
- Local archive post: encrypted-to-self, replicates across user's linked devices via the multi-device shared-posting-key mechanism
- UX: DMs appear as continuous thread in the UI despite wire-level rotation
- Group thread rotation
**Wire protocol:**
- No new message types — ephemeral posting IDs are just short-lived posting keys. The handshake field lives inside the encrypted payload.
**Verification:**
- Observer test: capture a DM thread's wire traffic; confirm no cryptographic tie between successive messages
- Resilience: drop a message mid-thread; confirm the next message catches up via the sliding window
- Archive test: scroll back through a 100-message thread; confirm all messages visible via local archive even though wire-level IDs are long forgotten
**Risks:**
- Complexity of rotation + archive is high. Lots of state to get right.
- UX: users need to understand that "they can't recover messages from a new device without importing the archive" — this is true already for any encrypted history, but doubly so here.
---
## Version promotion plan
- **0.5.3 is stable.** Last stable of the pre-split architecture. Maintenance-only (critical bugs, security). No new features.
- **0.6.0 through 0.6.5** are beta releases shipping Phases 1-6. Beta users form a network distinct from stable users.
- **0.7.0-beta** consolidates the complete new architecture; the legacy code paths are already removed as each phase shipped, so 0.7.0 is mostly polish + integration testing.
- **0.7.x-beta** gets real-world soak testing.
- **0.7.N → stable** once the new architecture is proven.
No "migration bridge" version is needed between 0.5 stable and 0.6 beta — users choose a track. Moving between tracks is possible via the existing export/import identity bundle (users can carry their content across, because the bundle format is preserved as a free compat concern).
## Order-of-operations recommendations
- Phases 1 and 2 can overlap (CDN restructure can happen while PostPush is being removed — they touch different code paths).
- Phase 3 depends on Phase 2 being in place (the file-holder refactor touches pull handling).
- Phase 4 is a prerequisite for 5 and 6 but doesn't need to wait for 1-3 (it's local/storage only, wire protocol unchanged).
- Phase 5 UX should wait for Phase 4 to ship and stabilize so persona-aware code is well tested.
- Phase 6 is the most complex; ship last.
## What must not regress
- Existing users upgrading from 0.5.3 should see no behavior change until they explicitly opt in to new features.
- Cross-version interoperability is required at every phase boundary.
- Data integrity: all migrations must be transaction-safe and reversible (keep old tables until N+1 phase).
## Not in scope for 0.6.x
- Search/discovery improvements (existing Worm still used)
- Anchor/directory redesign (separate planned track)
- Erasure-coded CDN replication (separate planned track)
- Reciprocity / Phase 2 economic features (still deferred)