| stepsCompleted |
|
||||||||
|---|---|---|---|---|---|---|---|---|---|
| status | complete | ||||||||
| completedAt | 2026-02-02 | ||||||||
| inputDocuments |
|
||||||||
| workflowType | architecture | ||||||||
| project_name | tom-protocol | ||||||||
| user_name | Malik | ||||||||
| date | 2026-02-02 |
This document builds collaboratively through step-by-step discovery. Sections are appended as we work through each architectural decision together.
Functional Requirements: 45 FRs across 8 capability areas. Core protocol (FR1-10) defines relay-based message transport with progressive features: known relay → dynamic routing → bidirectional → multi-relay → E2E encryption. UX layer (FR11-15) provides demo interface. Network participation (FR16-20) establishes dual client/relay node model. Bootstrap (FR21-24) handles temporary signaling. SDK (FR25-30) exposes two integration levels. LLM tooling (FR31-35) enables AI-first distribution. Community (FR36-40) supports micro-session contribution. Economy (FR41-45) tracks contribution/usage equilibrium.
Non-Functional Requirements: 14 NFRs drive architectural decisions: <500ms message delivery and <3s peer discovery set performance floor. Relay statelessness (pass-through only) eliminates server-side storage. 24h max backup with multi-node redundancy then delete defines temporary persistence model. Mandatory E2E from iteration 5 requires crypto pipeline from day one in design. Inversion property (more nodes = faster) is the core architectural differentiator — architecture must enable this, not merely tolerate it.
Scale & Complexity:
- Primary domain: distributed networking / protocol SDK
- Complexity level: high — novel protocol with P2P transport, dynamic role assignment, cryptographic pipeline, contribution economy
- Estimated architectural components: ~8 (transport layer, signaling/bootstrap, routing engine, crypto pipeline, node manager/role assignment, contribution tracker, SDK abstraction, demo UI)
- Browser-first: WebRTC as primary transport — no native UDP sockets in browsers
- Holepunch stack: HyperDHT + Hyperswarm for NAT traversal (native only). WebRTC ↔ UDX bridge required for browser nodes.
- No central infrastructure: Architecture must function with zero servers post-bootstrap. Bootstrap is temporary, isolated, documented.
- TypeScript/JavaScript first: PoC targets browsers and Node.js. Protocol design must remain language-agnostic.
- Progressive iteration model: Each layer builds on the previous. Architecture must support incremental capability addition without refactoring core.
- MIT License: No dependency on restrictively-licensed libraries.
- NAT traversal: Affects transport, discovery, bootstrap, and relay selection. Holepunch modules as foundation, WebRTC bridge as browser adaptation.
- Encryption pipeline: Touches transport, relay logic (must not read content), backup storage (encrypted at rest), and SDK abstraction (transparent to developer).
- Node identity & role assignment: Spans network participation, contribution tracking, relay selection, and reconnection handling.
- Statelessness & purge: Affects relay design (no persistence), backup design (24h TTL), subnet lifecycle (sliding genesis), and overall memory management.
- Dual client/relay nature: Every node must handle both sending/receiving and forwarding. No architectural separation between "client nodes" and "server nodes."
Protocol SDK — not a web application. No starter template applies. Manual scaffold, LLM-friendly.
- Language: TypeScript 5.x (strict mode)
- Runtime: Browser (WebRTC) + Node.js
- Framework: None — vanilla HTML/JS for demo. Zero framework dependency.
- Build: tsup (library bundling, ESM + CJS output)
- Tests: vitest (fast, TypeScript-native)
- Lint/Format: biome (single tool, fast, replaces ESLint + Prettier)
- Package manager: pnpm (workspaces support, strict dependency resolution)
- License: MIT
Rationale: Three packages share protocol types and need coordinated versioning. Monorepo from day one prevents painful extraction later. pnpm workspaces are minimal — no Nx/Turborepo overhead.
Structure:
tom-protocol/
├── packages/
│ ├── core/ # Raw protocol primitives (connect, send, receive, roles)
│ └── sdk/ # Plug-and-play abstraction (TomClient.create, auto-relay, auto-encrypt)
├── apps/
│ └── demo/ # Vanilla HTML/JS — click, start, chat. Zero friction.
├── pnpm-workspace.yaml
├── tsconfig.base.json
├── biome.json
└── package.json
Initialization: No CLI command. First implementation story scaffolds this structure manually.
Architectural Decisions Provided by This Choice:
- ESM-first with CJS fallback (tsup dual output)
- Strict TypeScript across all packages
- Shared tsconfig.base.json, per-package tsconfig.json
- vitest with workspace config for cross-package testing
- biome for consistent formatting and linting
- Demo app uses Vite dev server (vanilla mode, no framework) for hot reload during development
Critical Decisions (Block Implementation):
- Transport mechanism (WebRTC DataChannel via relay)
- Signaling bootstrap (WebSocket, temporary)
- Node identity (Ed25519 keypair, persisted)
- Internal node architecture (event-driven, unified client/relay)
Important Decisions (Shape Architecture): 5. Wire format (JSON, upgradeable) 6. Encryption stack (TweetNaCl.js, iteration 5+) 7. Role model (send/receive = user choice, relay/backup = network-imposed)
Deferred Decisions (Post-MVP): 8. DHT implementation for autonomous discovery 9. Contribution scoring formula 10. Subnet formation algorithm
ADR-001: WebRTC DataChannel via Relay
- All messages transit through at least one relay node, even in iteration 1
- No direct A→B in the protocol — the relay IS the architecture
- WebRTC DataChannels carry the data (binary-capable, browser-native, P2P after signaling)
- Rationale: Relays are not an optimization — they are the mechanism by which nodes contribute. Sending and receiving is a user action. Relaying is a network-imposed role.
ADR-002: Signaling Bootstrap
- Demo/PoC (iterations 1-3): Single WebSocket server (Node.js, ~50 lines). Fixed endpoint. The umbilical cord.
- Growth (iterations 4-5): Multiple signaling seeds (still WebSocket, several VPS). Resilience through redundancy.
- Alpha (iteration 6): Distributed hash table begins operating among 10-15 nodes. Seeds become regular nodes.
- Target: The signaling "phone number" (topic hash) stays the same. The "secretary" who answers changes dynamically. No one knows who will answer next. No one can corrupt her in advance. If she disappears, the network appoints another.
- Final state: Zero fixed infrastructure. The network IS the infrastructure. The VPS is the umbilical cord — cut when the baby breathes alone.
ADR-003: Wire Format
- JSON for iterations 1-4 (human-readable, debug-friendly, native JS)
- Envelope structure:
{from, to, via, type, payload, timestamp, signature} - Protocol layer abstracts the format — swappable to MessagePack or binary without breaking SDK
- Migration to compact format when performance requires it (iteration 5+)
ADR-004: Encryption Stack
- Library: TweetNaCl.js — minimal, audited, browser + Node.js compatible
- Key exchange: X25519 (Diffie-Hellman on Curve25519)
- Symmetric encryption: ChaCha20-Poly1305 (authenticated encryption)
- Handshake: Custom protocol inspired by Noise XX pattern
- Timeline: Crypto pipeline designed from iteration 1, activated at iteration 5
- Rationale: No heavy dependencies. Same primitives as Holepunch's SecretStream. Battle-tested algorithms.
ADR-005: Node Identity
- Ed25519 keypair generated on first join
- Public key = node identity on the network (no central registry)
- Browser: Persisted in localStorage
- Node.js: Persisted in file (~/.tom/identity.json)
- Reconnection (FR45): same keypair = same identity = pending messages delivered
- No accounts, no registration, no identity server. Your key is your address.
ADR-006: Unified Node Model (Event-Driven)
- Every node runs the same code — no "client binary" vs "relay binary"
- Internal components:
TransportLayer— manages WebRTC DataChannels (connections to peers)Router— receives all messages, decides: for me → deliver, not for me → forwardRoleManager— tracks assigned role (client, relay, backup, guardian, validator). Role is imposed by network, not chosen.MessageHandler— processes messages destined for this nodeContributionTracker— maintains contribution/usage score
- Message flow: Transport → Router → (MessageHandler | forward via Transport)
- The topology creates the role, not the code. No if/else "am I a relay?"
ADR-007: Role Model
- User-controlled: Send messages, receive messages, choose username, view path details
- Network-imposed: Relay (forward others' messages), backup (temporarily store undelivered messages), guardian, validator
- A node does not volunteer to relay. The network assigns relay duty based on availability, score, and unpredictable selection.
- Rationale: This is ToM's core differentiator from BitTorrent (optional seeding) and Nostr (volunteer relays). Contribution is mandatory and invisible.
ADR-008: Demo Hosting (Temporary)
- Demo UI: GitHub Pages (static, free, reliable) — vanilla HTML/JS
- Signaling server: Minimal VPS (~5€/month, Hetzner/OVH) running Node.js WebSocket
- Both are transitional. The demo page will eventually be served by the ToM network itself (self-hosting). The signaling VPS will be replaced by distributed DHT.
- This is the accepted compromise documented in the PRD. Isolated, temporary, marked for elimination.
Implementation Sequence:
- Monorepo scaffold (pnpm workspace, tsconfig, biome, vitest)
- Node identity generation (Ed25519 keypair)
- Transport layer (WebRTC DataChannel establishment via signaling)
- Router + MessageHandler (receive, decide, forward)
- Signaling server (WebSocket, Node.js)
- Demo UI (vanilla HTML/JS, Vite)
- RoleManager (iteration 2+)
- ContributionTracker (iteration 4+)
- Crypto pipeline (iteration 5)
Cross-Component Dependencies:
- Transport depends on Signaling (for WebRTC SDP/ICE exchange)
- Router depends on Transport (for message ingestion)
- RoleManager depends on Router (to know what traffic flows through)
- ContributionTracker depends on RoleManager (to measure contribution)
- Crypto pipeline wraps Transport (encrypt before send, decrypt after receive)
- SDK wraps all of the above into
TomClient.create()/.send()/.onMessage()
Critical Conflict Points Identified: 23 areas where AI agents could make different choices across naming, structure, format, communication, and process categories.
File & Directory Naming:
- All files:
kebab-case.ts(e.g.,transport-layer.ts,role-manager.ts) - Test files:
kebab-case.test.tsco-located next to source file - Index files:
index.tsfor public API re-exports only - No
utils.tsorhelpers.ts— name by purpose (e.g.,message-validation.ts)
Code Naming:
- Classes/Interfaces/Types:
PascalCase(e.g.,TransportLayer,MessageEnvelope) - Functions/Methods:
camelCase(e.g.,forwardMessage,assignRole) - Constants:
UPPER_SNAKE_CASE(e.g.,MAX_RELAY_HOPS,DEFAULT_TTL) - Variables:
camelCase(e.g.,peerConnection,relayNode) - Private members: no underscore prefix — use TypeScript
privatekeyword - Boolean variables: prefix with
is,has,can,should(e.g.,isRelaying,hasIdentity) - Event names:
kebab-casestrings (e.g.,message-received,peer-connected,role-assigned)
No abbreviations except universally understood ones: id, msg, config, ctx. Spell everything else out.
Module Organization:
- Each component is a single directory with
index.tspublic API - Internal files are private — never import from another component's internals
- Dependency direction: Transport ← Router ← MessageHandler (never reverse)
- Circular dependencies are forbidden — use events to decouple
Test Organization:
- Co-located:
transport-layer.ts→transport-layer.test.ts(same directory) - Test fixtures:
__fixtures__/directory within component directory - Integration tests:
packages/<pkg>/tests/integration/
Export Rules:
- Each package has a single
src/index.tsentry point - Only export what consumers need — internal types stay internal
- Re-export types explicitly:
export type { MessageEnvelope }(notexport *)
Message Envelope (Wire Format):
interface MessageEnvelope {
id: string; // UUIDv4, generated by sender
from: string; // Sender public key (hex)
to: string; // Recipient public key (hex)
via: string[]; // Ordered relay path (public keys)
type: string; // Message type (e.g., "chat", "signal", "role-assign")
payload: unknown; // Type-specific content
timestamp: number; // Unix epoch milliseconds
signature: string; // Ed25519 signature of envelope (hex)
}Internal Event Payloads:
// All internal events follow this shape
interface InternalEvent<T = unknown> {
source: string; // Component name that emitted
timestamp: number; // Unix epoch ms
data: T; // Typed payload
}Error Format:
class TomError extends Error {
constructor(
message: string,
public readonly code: TomErrorCode,
public readonly context?: Record<string, unknown>
) {
super(message);
this.name = 'TomError';
}
}
type TomErrorCode =
| 'TRANSPORT_FAILED'
| 'PEER_UNREACHABLE'
| 'SIGNALING_TIMEOUT'
| 'INVALID_ENVELOPE'
| 'IDENTITY_MISSING'
| 'RELAY_REJECTED'
| 'CRYPTO_FAILED';Date/Time: Always Unix epoch milliseconds (Date.now()). Never ISO strings in protocol messages. ISO only for human-facing logs.
JSON Fields: camelCase everywhere — wire format, internal state, API responses.
Inter-Component Communication:
- Components communicate via typed
EventEmitter— never direct method calls between sibling components - Router does not import MessageHandler. Router emits
message-for-local, MessageHandler listens. - TransportLayer emits
data-received, Router listens. - RoleManager emits
role-changed, any interested component listens.
Event Naming Convention:
- Format:
noun-verb-past(e.g.,peer-connected,message-forwarded,role-assigned) - Never future tense (
will-connect) or imperative (connect-peer)
State Management:
- Immutable updates only — never mutate state objects in place
- Each component owns its state — no shared mutable state between components
- State reads via getter methods, state changes via events
Error Handling:
- Always throw
TomErrorwith appropriate code — never rawError - Components catch errors at their boundary and emit error events
- Transport errors → retry with exponential backoff (max 3 attempts, 1s/2s/4s)
- Unrecoverable errors → emit
fatal-errorevent, let parent handle - Never swallow errors silently — at minimum, log and re-emit
Logging:
- Use structured logging with component prefix:
[TransportLayer],[Router],[RoleManager] - Log levels:
debug(development only),info(state changes),warn(recoverable issues),error(failures) - Log message received/forwarded/delivered at
debuglevel - Log peer connect/disconnect at
infolevel - Log role changes at
infolevel - Never log private keys or full message payloads at any level
Async Patterns:
- All async operations return
Promise— no callbacks - Use
async/awaitexclusively — no.then()chains - Cleanup: every component implements
destroy(): Promise<void>for graceful shutdown - Cancel in-flight operations on destroy — no dangling promises
All AI Agents MUST:
- Follow naming patterns exactly — no variations, no "creative" naming
- Use
TomErrorfor all error cases — never throw plain strings or generic Error - Communicate between components via events only — no direct imports of sibling internals
- Co-locate tests with source files
- Export only through package
index.ts - Use
async/awaitfor all async code - Include component prefix in all log statements
Pattern Verification:
- Biome config enforces naming conventions (configured in
biome.json) - TypeScript strict mode catches type violations
- Package
index.tsis the only allowed import path for external consumers - Code review checklist: naming, error handling, event patterns, no circular deps
Good Examples:
// Correct: event-driven, typed, proper naming
class Router {
private readonly events: TypedEventEmitter;
async handleIncoming(envelope: MessageEnvelope): Promise<void> {
if (envelope.to === this.localIdentity) {
this.events.emit('message-for-local', {
source: 'Router',
timestamp: Date.now(),
data: envelope,
});
} else {
await this.forward(envelope);
}
}
}Anti-Patterns:
// WRONG: direct import of sibling internal
import { processMessage } from '../message-handler/process';
// WRONG: raw Error
throw new Error('connection failed');
// WRONG: callback
transport.onData((data) => { ... });
// WRONG: mutable shared state
export const globalState = { peers: [] };
// WRONG: any type
function handle(msg: any) { ... }
// WRONG: console.log
console.log('message received');tom-protocol/
├── package.json # Root workspace config
├── pnpm-workspace.yaml # Workspace: packages/*, apps/*
├── tsconfig.base.json # Shared TS config (strict, ESM)
├── biome.json # Lint + format (all packages)
├── vitest.workspace.ts # Cross-package test config
├── .gitignore
├── LICENSE # MIT
├── README.md
│
├── packages/
│ ├── core/ # Raw protocol primitives
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ ├── tsup.config.ts # ESM + CJS dual output
│ │ └── src/
│ │ ├── index.ts # Public API: TransportLayer, Router, etc.
│ │ ├── identity/
│ │ │ ├── index.ts
│ │ │ ├── keypair.ts # Ed25519 generation & persistence
│ │ │ └── keypair.test.ts
│ │ ├── transport/
│ │ │ ├── index.ts
│ │ │ ├── transport-layer.ts # WebRTC DataChannel management
│ │ │ ├── transport-layer.test.ts
│ │ │ ├── signaling-client.ts # WebSocket signaling connection
│ │ │ └── signaling-client.test.ts
│ │ ├── router/
│ │ │ ├── index.ts
│ │ │ ├── router.ts # Message routing: local vs forward
│ │ │ └── router.test.ts
│ │ ├── message/
│ │ │ ├── index.ts
│ │ │ ├── message-handler.ts # Process messages destined for this node
│ │ │ ├── message-handler.test.ts
│ │ │ ├── envelope.ts # MessageEnvelope type & validation
│ │ │ └── envelope.test.ts
│ │ ├── roles/
│ │ │ ├── index.ts
│ │ │ ├── role-manager.ts # Track & respond to role assignments
│ │ │ └── role-manager.test.ts
│ │ ├── contribution/
│ │ │ ├── index.ts
│ │ │ ├── contribution-tracker.ts # Usage/contribution score
│ │ │ └── contribution-tracker.test.ts
│ │ ├── crypto/ # Iteration 5+ (designed now, activated later)
│ │ │ ├── index.ts
│ │ │ ├── handshake.ts # X25519 key exchange
│ │ │ ├── encryption.ts # ChaCha20-Poly1305 encrypt/decrypt
│ │ │ └── handshake.test.ts
│ │ ├── errors/
│ │ │ ├── index.ts
│ │ │ └── tom-error.ts # TomError class + TomErrorCode
│ │ ├── events/
│ │ │ ├── index.ts
│ │ │ └── typed-emitter.ts # Typed EventEmitter wrapper
│ │ └── types/
│ │ └── index.ts # Shared types (MessageEnvelope, etc.)
│ │
│ └── sdk/ # Plug-and-play abstraction
│ ├── package.json
│ ├── tsconfig.json
│ ├── tsup.config.ts
│ └── src/
│ ├── index.ts # Public API: TomClient
│ ├── tom-client.ts # TomClient.create(), .send(), .onMessage()
│ ├── tom-client.test.ts
│ ├── auto-relay.ts # Automatic relay assignment
│ ├── auto-relay.test.ts
│ ├── auto-encrypt.ts # Transparent E2E (iteration 5+)
│ └── types/
│ └── index.ts # SDK-specific public types
│
├── apps/
│ └── demo/ # Vanilla HTML/JS — click, start, chat
│ ├── package.json
│ ├── vite.config.ts # Vanilla mode, no framework
│ ├── index.html
│ ├── src/
│ │ ├── main.ts # Entry: TomClient.create() + UI wiring
│ │ ├── ui.ts # DOM manipulation (connect, send, display)
│ │ └── style.css # Minimal styling
│ └── public/
│
└── tools/
└── signaling-server/ # Bootstrap WebSocket server (~50 lines)
├── package.json
├── tsconfig.json
├── src/
│ ├── index.ts # Entry: start WS server
│ ├── server.ts # WebSocket SDP/ICE relay
│ └── server.test.ts
└── Dockerfile # Optional: deploy to VPS
Package Boundaries:
- core → zero external dependency (except tweetnacl). Does not know sdk exists. Does not know demo exists.
- sdk → depends on core only. Exposes
TomClientas sole public API. Hides all protocol complexity. - demo → depends on sdk only. Never imports from core directly.
- signaling-server → independent. Zero dependency on any package. Pure WebSocket SDP/ICE relay.
Dependency Direction (strict, never reversed):
demo → sdk → core
↑
signaling-server (independent, no dependency on packages)
Component Boundaries (within core):
- Components communicate via typed EventEmitter only — no direct imports between siblings
- Transport → emits
data-received→ Router listens - Router → emits
message-for-local→ MessageHandler listens - RoleManager → emits
role-changed→ any component listens - Each component directory has
index.tsas sole public entry point
| FR Category | Package | Directory |
|---|---|---|
| FR1-10 (Core protocol) | core | transport/, router/, message/, roles/ |
| FR11-15 (UX/Demo) | demo | apps/demo/src/ |
| FR16-20 (Network participation) | core | roles/, contribution/ |
| FR21-24 (Bootstrap) | signaling-server + core | tools/signaling-server/, transport/signaling-client |
| FR25-30 (SDK) | sdk | tom-client, auto-relay, auto-encrypt |
| FR31-35 (LLM tooling) | sdk | types/ (well-documented for AI consumption) |
| FR36-40 (Community) | core | contribution/ |
| FR41-45 (Economy) | core | contribution/ |
Demo UI → TomClient.send() → TransportLayer → [WebRTC] → Relay Node → [WebRTC] → Router → MessageHandler → TomClient.onMessage() → Demo UI
↑
SignalingClient ← [WebSocket] ← SignalingServer (bootstrap only)
Configuration Files:
- Root: workspace-level config (pnpm, tsconfig base, biome, vitest workspace)
- Per-package: package.json, tsconfig.json, tsup.config.ts
- No
.envfiles — signaling server URL is a constructor parameter, not environment config
Test Organization:
- Unit tests: co-located (
*.test.tsnext to source) - Integration tests:
packages/<pkg>/tests/integration/ - Fixtures:
__fixtures__/within component directory - No e2e test directory yet — added when demo reaches iteration 3+
Build Output:
- Each package builds to
dist/(gitignored) - tsup produces ESM (
dist/index.mjs) + CJS (dist/index.cjs) + types (dist/index.d.ts) - Demo builds to
apps/demo/dist/(static files for GitHub Pages)
Decision Compatibility: All technology choices (TypeScript 5.x strict, tsup, vitest, biome, WebRTC, TweetNaCl.js) are mutually compatible. All run natively in browser + Node.js. No version conflicts. JSON wire format is native to both runtimes.
Pattern Consistency: Implementation patterns (event-driven communication, typed EventEmitter, kebab-case files, PascalCase classes) directly support the unified node model (ADR-006). No contradictions between patterns and decisions.
Structure Alignment: Project structure (packages/core, packages/sdk, apps/demo, tools/signaling-server) maps cleanly to dependency direction. Each ADR has a clear home in the structure. No orphan decisions.
Functional Requirements Coverage:
| FR Category | Coverage | Notes |
|---|---|---|
| FR1-10 (Core protocol) | ✅ Full | transport/, router/, message/, roles/ |
| FR11-15 (UX/Demo) | ✅ Full | apps/demo/ via TomClient |
| FR16-20 (Network participation) | ✅ Full | roles/, contribution/ |
| FR21-24 (Bootstrap) | ✅ Full | signaling-server + signaling-client |
| FR25-30 (SDK) | ✅ Full | sdk/tom-client, auto-relay, auto-encrypt |
| FR31-35 (LLM tooling) | ✅ Full | sdk/types/ well-documented |
| FR36-40 (Community) | ✅ Full | contribution/ |
| FR41-45 (Economy) | ✅ Full | contribution/ |
Non-Functional Requirements Coverage:
| NFR | Coverage | Mechanism |
|---|---|---|
| <500ms message delivery | ✅ | WebRTC DataChannel (sub-100ms native) |
| <3s peer discovery | ✅ | WebSocket signaling, future DHT |
| Relay statelessness | ✅ | ADR-006 unified node, pass-through only |
| 24h backup TTL | ✅ | Backup node mechanism (see ADR-009 below) |
| E2E encryption | ✅ | crypto/ directory, TweetNaCl.js (iteration 5) |
| Inversion property | ✅ | ADR-007 mandatory relay, more nodes = more capacity |
ADR-009: Message Backup & Survival Strategy
Clarified during validation — the backup mechanism follows the "virus" metaphor from the whitepaper:
- Role assignment: Backup is a network-imposed role, like relay. The network decides who becomes backup based on available storage, time online, bandwidth, and contribution score. Cascading — never a single backup node.
- Multi-node replication: Messages are replicated across multiple backup nodes for resilience. If one backup disconnects, others still hold the message.
- Message scoring & host-hopping: Each buffered message has a survival score based on host quality (timezone alignment with recipient, connection history, bandwidth). The message is proactive, not reactive — it monitors its own score continuously. When the score drops, the message replicates to a better host in parallel (doesn't wait for completion before acting). When the score drops below a threshold X%, the message self-deletes from the current host before the host dies — because waiting for the host to disconnect is already too late. The message does everything in its power to survive 24h.
- TTL enforcement: 24h maximum. After TTL, all backup copies are purged. No permanent storage anywhere.
- Recipient reconnection: When the recipient comes back online, it queries the network for pending messages. Upon successful delivery, a "received" signal propagates through the network, triggering deletion of all backup copies.
- Iteration: This mechanism is designed now but implemented at iteration 3+. The architecture supports it via contribution/ and roles/ in core.
Critical Gaps: 0
Important Gaps (all acceptable for PoC):
- Holepunch bridge — not yet in structure. Will be
transport/holepunch-bridge.tsat iteration 6+. Architecture supports it. - CI/CD pipeline — no
.github/workflows/defined. Added during scaffold story.
Nice-to-Have Gaps:
- Monitoring/observability patterns — acceptable for PoC
- Rate limiting on signaling server — acceptable for demo scale
✅ Requirements Analysis
- Project context thoroughly analyzed (45 FRs, 14 NFRs)
- Scale and complexity assessed (high — novel protocol)
- Technical constraints identified (browser-first, no central infra, MIT)
- Cross-cutting concerns mapped (NAT, encryption, identity, statelessness, dual nature)
✅ Architectural Decisions
- 9 ADRs documented with rationale
- Technology stack fully specified with versions
- Integration patterns defined (event-driven, typed EventEmitter)
- Performance considerations addressed (WebRTC latency, JSON parse speed)
✅ Implementation Patterns
- Naming conventions established (23 conflict points resolved)
- Structure patterns defined (module organization, exports, tests)
- Communication patterns specified (event-driven, noun-verb-past)
- Process patterns documented (TomError, logging, async/await)
✅ Project Structure
- Complete directory structure defined (every file listed)
- Component boundaries established (strict dependency direction)
- Integration points mapped (event flow between components)
- Requirements to structure mapping complete (all 45 FRs mapped)
Overall Status: READY FOR IMPLEMENTATION
Confidence Level: High
Key Strengths:
- Progressive iteration model — each component can be built and tested independently
- Event-driven architecture eliminates coupling between components
- Virus-like message survival strategy is architecturally novel and well-defined
- Zero framework dependency keeps the project lean and portable
- Unified node model means no code duplication between client and relay
Areas for Future Enhancement:
- CI/CD pipeline (GitHub Actions) — scaffold story
- Holepunch bridge for native transport — iteration 6
- Monitoring & observability — when network exceeds 10 nodes
- Message scoring algorithm refinement — iteration 3+
AI Agent Guidelines:
- Follow all 9 ADRs exactly as documented
- Use implementation patterns consistently across all components
- Respect package boundaries: demo → sdk → core (never reversed)
- Communicate between components via typed EventEmitter only
- Refer to this document for all architectural questions
First Implementation Priority:
- Monorepo scaffold (pnpm workspace, tsconfig, biome, vitest)
- Node identity generation (Ed25519 keypair)
- Transport layer + signaling client
- Router + MessageHandler
- Signaling server
- Demo UI