# Network Architecture Canonical capability spec: [`openspec/specs/network/spec.md`](../../openspec/specs/network/spec.md) (`NET-01..NET-125`). ## The five core abstractions | Abstraction | Role | Spec | | --- | --- | --- | | `NetworkPlugin` | The universal extension contract every plugin implements | NET-01 | | `NetworkNode` | The single entry-point — owns lifecycle, bus, router, transports | NET-02 | | `MessageEnvelope` | Typed wire format for every message | NET-03 | | `Transport` | The contract every transport plugin satisfies | NET-04 | | `NetworkBus` | The cross-plugin event broker (no plugin imports another plugin) | NET-05 | These are the only five things `network-core` ships. Everything else — even WebRTC — is a plugin. ## `NetworkPlugin` (NET-01) ```ts interface NetworkPlugin { readonly name: string; install(node: NetworkNode): Promise | void; start(): Promise | void; stop(): Promise | void; dispose?(): Promise | void; } ``` Lifecycle is strict: `install → start → ... → stop → dispose`. The core calls them in that order; plugins must be idempotent across `start`/`stop` (you can restart without disposing). **Plugins do not import each other.** Cross-plugin communication goes through `NetworkBus`. ## `NetworkNode` (NET-02) ```ts class NetworkNode { use(plugin: NetworkPlugin): this; start(): Promise; stop(): Promise; send(targetId: PeerId, envelope: MessageEnvelope): Promise; broadcast(envelope: MessageEnvelope): Promise; on(event: E, handler: Handler): () => void; readonly bus: NetworkBus; readonly router: MessageRouter; bootstrap(options: BootstrapOptions): Promise; // NET-113 } ``` The node owns plugin lifecycle, transport pool, and message routing. Sending is transport-agnostic — the node hands the envelope to whichever active transport can reach the target peer. ## `MessageEnvelope` (NET-03) ```ts interface MessageEnvelope { id: string; // unique type: string; // e.g. 'cd:request', 'election:vote' senderId: PeerId; targetId: PeerId | "*"; // "*" = broadcast payload: Uint8Array; // already-encoded; protocol plugin handles serialisation timestamp: number; // injectable Clock headers: Record; } ``` Validated via Zod before routing. The 13-byte binary header from `network-plugin-protocol` (`NET-15`) prefixes this when serialising for the wire: `version(1) | flags(1) | messageType(1) | timestamp(8) | payloadLength(2)`. ## `Transport` (NET-04) ```ts interface Transport { connect(peerId: PeerId, address: string): Promise; disconnect(peerId: PeerId): Promise; send(peerId: PeerId, envelope: MessageEnvelope): Promise; onMessage(handler: (env: MessageEnvelope) => void): () => void; getConnectedPeers(): PeerId[]; } ``` Concrete transports: WebRTC (`network-plugin-webrtc`), WebSocket (`network-websockets`), HTTPS (planned), libp2p (`network-libp2p` — pending), Hyperswarm (`network-hyperswarm` — pending), Trystero (`network-discovery-trystero` — pending), GunDB (`network-discovery-gundb` — pending), MemoryTransport (test-only, in `network-testing`). `network-plugin-transport-manager` (NET-23..NET-26) maintains multiple active transports per peer simultaneously, exchanges capability bitmaps during handshake (NET-24), and routes messages by priority across them (NET-25). CRITICAL messages may go redundantly across two transports; BULK uses backpressure-aware selection. ## `NetworkBus` (NET-05) Typed pub/sub with FIFO delivery, bounded-queue backpressure, and interceptor support. Topics are constants from the frozen `NetworkEvents` enum (NET-55): ```text node:created, node:connected, node:disconnected, node:updated, node:removed, message:sent, message:received, message:routed, message:dropped, hierarchy:changed, hierarchy:parent-changed, hierarchy:child-added, ..., plugin:registered, plugin:unregistered, peer:joined, peer:left, peer:updated, peer:latency, transport:connected, transport:disconnected, election:vote, election:completed, election:cancelled, failover:triggered, failover:completed, topology:changed, latency:rebalance, cd:advertise, cd:request, cd:response, partition:detected, partition:healed, qos:congestion, monitor:alert, system:error ``` Plugins subscribe; plugins publish; the bus enforces ordering and backpressure. **No plugin ever imports another plugin's code.** That is the wire that lets `network-suite` compose 21 plugins without circular deps. ## `MessageRouter` Owns: - Per-message type → handler dispatch. - TTL decrement (`NET-58`); drop when `ttl <= 0`. - Loop detection via `visitedNodes` header (`NET-58`). - Broadcast deduplication via `messageId` bloom filter. - Route caching for frequently-used `(source, target) → nextHop` mappings (`NET-59`); invalidated on `topology:changed`. ## How a plugin actually talks to another plugin Concrete example — `drive-mesh` wants a chunk from a peer: ```text drive-mesh.fetchChunk(fileId, chunkId) ↓ networkNode.bus.publish('cd:request', { fileId, chunkId, requesterId }) ↓ network-plugin-content-delivery.onCdRequest(...) ↓ resolves a peer holding the chunk via gossip index ↓ networkNode.send(holderPeerId, { type: 'cd:fetch', payload: { fileId, chunkId } }) ↓ (transport-manager picks WebRTC, WebSocket, or HTTPS based on capability negotiation) ↓ holder runs network-plugin-content-delivery.onCdFetch and sends back chunk bytes ↓ networkNode.bus.publish('cd:response', { fileId, chunkId, bytes }) ↓ drive-mesh.onCdResponse(...) → verify sig + AEAD via chunks plugin → return to caller ``` Three plugins coordinated; none imported any other; all communication via bus topics. Replace `network-plugin-content-delivery` with a different chunk-distribution implementation and the rest of the chain doesn't notice. ## `BootstrapOptions` (NET-113, NET-114) ```ts interface BootstrapOptions { mode: 'cold' | 'warm'; initialPeers?: PeerAddress[]; bootstrapServers?: string[]; connectionConcurrency?: number; discoveryTimeout?: number; prioritizeLocalNetwork?: boolean; } ``` `NetworkNode.bootstrap(options)` brings up the node from a clean state — does discovery, opens initial connections, and (with `ConnectionPreWarmer`) pre-opens transports to known peers before they are needed. ## `PluginRegistry` and `PluginDiscovery` (NET-116) ```ts class PluginRegistry { register(plugin: NetworkPlugin): void; unregister(name: string): void; has(name: string): boolean; get(name: string): NetworkPlugin | undefined; list(): NetworkPlugin[]; } class PluginDiscovery { findPluginsByCapability(capability: string): NetworkPlugin[]; } ``` Runtime plugin discovery and registration — the same pattern the rest of the monorepo uses (storage factory, db factory, etc.). ## `TransportTester` PDK (NET-115) Third-party transport plugins are validated by a conformance harness: ```ts class TransportTester { testBasicFunctionality(transport: Transport): Promise; testReliability(transport: Transport): Promise; testBackpressure(transport: Transport): Promise; testReconnection(transport: Transport): Promise; } ``` Anyone shipping a custom transport (e.g., a corporate VPN tunnel transport) runs `TransportTester` against it before registering with `network-plugin-transport-manager`. This is what makes "swap WebRTC for $YOUR_TRANSPORT" a one-line change. ## Why this shape The core's job is to do **as little as possible**. It owns plugin lifecycle, the bus, the router, the envelope shape, and the transport contract — and stops. Every protocol decision (binary header layout, encryption, signing, election algorithm, partition recovery) lives in a plugin where it can be replaced. This is the lesson the old planning corpus was groping toward but never quite landed: a "core + plugins" model where the core is genuinely small and the plugins genuinely don't depend on each other. The `network-suite` change folder finally delivers it; the canonical spec encodes it; this is the design genesis ships. ## Reading further - [`topology-and-failover.md`](./topology-and-failover.md) — the resilience model. - [`plugins-catalog.md`](./plugins-catalog.md) — what each plugin does. - [`security-and-tokens.md`](./security-and-tokens.md) — the trust model.