P2P
In the previous chapter, you learned offline-first sync. Now you will learn P2P.
P2P means peer-to-peer. Instead of every node depending on a central server, nodes can discover each other, connect, exchange messages, and synchronize data.
node A ↔ node B
local write → WAL → outbox → P2P transport → peer → ackWhy P2P exists
Real systems do not always have a perfect cloud connection. P2P gives Vix a way to build systems that communicate directly.
Use P2P when you want: local network discovery, direct node-to-node communication, peer synchronization, edge replication, offline-first data exchange, store-and-forward systems.
The mental model
| Layer | Purpose |
|---|---|
Discovery | Finds available peers on the network. |
Peer connection | Connects the local node to another peer. |
Handshake | Establishes peer identity and session metadata. |
Envelope | Wraps messages with routing and protocol metadata. |
Framing | Splits byte streams into complete protocol messages. |
Dispatch | Decodes payloads and routes them to typed handlers. |
Sync messages | Pushes WAL entries, sends acknowledgments, and pulls work. |
Public headers
#include <vix/p2p.hpp>
#include <vix/p2p_http.hpp> // for HTTP control routesMessage flow
typed message → payload bytes → envelope → frame → transport
→ peer → decode frame → decode envelope → dispatch typed messageEnvelope and framing
#include <vix/p2p.hpp>
// Create and pack a Ping message
vix::p2p::msg::Ping ping;
ping.nonce = 42;
vix::p2p::Envelope outgoing = vix::p2p::pack::make_envelope(vix::p2p::MessageType::Ping, ping);
// Frame it (length-prefix framing)
vix::p2p::framing::LengthPrefixVarint framer;
vix::p2p::Frame frame = framer.encode(outgoing.encode());
// Decode frame → envelope → message
vix::p2p::FrameDecodeResult decoded = framer.decode(frame.bytes);
vix::p2p::Envelope incoming = vix::p2p::Envelope::decode_or_throw(decoded.frames.front().bytes);
vix::p2p::msg::Ping roundtrip = vix::p2p::msg::Ping::decode_or_throw(incoming.payload);TCP is a byte stream — framing ensures the receiver knows where each message begins and ends.
Handshake messages
// Node A → Hello
vix::p2p::msg::Hello hello;
hello.nonce_a = 1001;
hello.node_id = "node-a";
hello.capabilities["proto"] = "1.0";
// Node B → HelloAck
vix::p2p::msg::HelloAck ack;
ack.nonce_a = 1001;
ack.nonce_b = 2002;
// Node A → HelloFinish
vix::p2p::msg::HelloFinish finish;
finish.nonce_a = 1001;
finish.nonce_b = 2002;
finish.signature = /* 64-byte signature */;Discovery announcement
vix::p2p::msg::DiscoveryAnnounce announce;
announce.node_id = "node-a";
announce.tcp_port = 9001;
announce.ts_ms = 1710000000000ULL;
announce.nonce = 987654321ULL;
announce.capabilities["proto"] = "1.0";
const std::string json = announce.to_json();
auto parsed = vix::p2p::msg::DiscoveryAnnounce::from_json(json);Memory router
vix::p2p::MemoryRouter router;
router.upsert_route("node-b", vix::p2p::Route{"edge-1", false, 8});
router.upsert_route("node-c", vix::p2p::Route{"relay-7", true, 4});
auto route = router.resolve("node-b"); // next_hop="edge-1", via_relay=false
router.remove_route("node-c");WAL sync messages
// Push WAL records to a peer
vix::p2p::msg::WalPush push;
push.seq_begin = 10;
push.seq_end = 12;
push.wal_bytes = make_fake_wal_bytes();
// Acknowledge applied sequence
vix::p2p::msg::WalAck ack;
ack.last_applied_seq = 12;
// Ask peer for pending operations
vix::p2p::msg::OutboxPull pull;
pull.target_node_id = "node-b";
pull.max_items = 64;Start a real P2P node
vix::p2p::NodeConfig cfg;
cfg.node_id = "node-a";
cfg.listen_port = 9001;
cfg.on_log = [](std::string_view line) { std::cout << line << "\n"; };
auto node = vix::p2p::make_tcp_node(cfg);
node->start();
const auto stats = node->stats();
// stats.peers_total, peers_connected, handshakes_started, handshakes_completedP2PRuntime and connect
vix::p2p::P2PRuntime runtime(node);
runtime.start();
vix::p2p::PeerEndpoint ep;
ep.host = "127.0.0.1";
ep.port = 9001;
ep.scheme = "tcp";
runtime.connect(ep);
// runtime.stats(), runtime.stop()UDP discovery
vix::p2p::DiscoveryConfig cfg;
cfg.self_node_id = "node-a";
cfg.self_tcp_port = 9001;
cfg.discovery_port = 37020;
cfg.mode = vix::p2p::DiscoveryMode::Broadcast;
cfg.announce_interval_ms = 1000;
auto on_peer = [](const vix::p2p::DiscoveryAnnouncement &a)
{
std::cout << "discovered: " << a.node_id << " at " << a.host << ":" << a.port << "\n";
};
auto discovery = vix::p2p::make_udp_discovery(cfg, on_peer);
discovery->start();
auto snapshot = discovery->snapshot();
discovery->stop();P2P HTTP control surface
#include <vix.hpp>
#include <vix/p2p.hpp>
#include <vix/p2p_http.hpp>
vix::p2p::P2PRuntime runtime(node);
runtime.start();
vix::p2p_http::P2PHttpOptions opt;
opt.prefix = "/p2p";
opt.enable_ping = true;
opt.enable_status = true;
opt.enable_peers = true;
vix::App app;
vix::p2p_http::registerRoutes(app, runtime, opt);
app.listen(8081, []() { std::cout << "HTTP API listening on 8081\n"; });HTTP control routes
| Route | Purpose |
|---|---|
GET /p2p/ping | Runs a simple P2P smoke test. |
GET /p2p/status | Returns current runtime statistics. |
GET /p2p/peers | Lists known peers for this node. |
POST /p2p/connect | Connects this node to another peer. |
GET /p2p/logs | Returns recent runtime log entries. |
curl http://127.0.0.1:8081/p2p/ping
curl http://127.0.0.1:8081/p2p/status
curl -X POST http://127.0.0.1:8083/p2p/connect \
-H "content-type: application/json" \
-d '{"host":"127.0.0.1","port":9201,"scheme":"tcp"}'Important vix run rule
# Correct — --run passes args to your program
vix run p2p_manual_connect.cpp --run server
vix run p2p_manual_connect.cpp --run client 127.0.0.1 9101
# Wrong — -- is for compiler/linker flags
vix run p2p_manual_connect.cpp -- serverHow P2P connects to sync
P2P can become one transport for the sync engine:
WAL → WalPush → peer receives → peer applies → WalAckThe P2P layer moves sync data between nodes. The sync layer decides what must be durable, retried, replayed, and acknowledged.
P2P vs WebSocket vs HTTP
| Criteria | WebSocket | P2P | HTTP |
|---|---|---|---|
| Connection | Client -> server. | Node -> node. | Request -> response. |
| Best for | Browser real-time apps. | Distributed systems. | Standard APIs. |
| Discovery | Manual configuration. | UDP discovery or registry. | Manual configuration. |
Common mistakes
Sending raw bytes without framing
TCP does not preserve message boundaries. Use framing before decoding.
Trusting unknown peers
Use handshake, identity, and security checks before trusting a peer.
Forgetting idempotency
A peer can receive the same sync message more than once. Use operation ids and WAL sequence numbers to deduplicate.
Exposing P2P HTTP control routes without auth
Routes like POST /p2p/connect must be protected in production.
Production notes
- Use stable node ids
- Protect control routes with authentication
- Use secure transport
- Make sync messages idempotent
- Monitor peer counts and handshake failures
- Log peer ids and operation ids
{
"ok": true,
"p2p": {
"node_id": "node-a",
"peers_total": 3,
"peers_connected": 2,
"handshakes_completed": 2
}
}What you should remember
The basic message flow: typed message → envelope → frame → transport → peer → decode → dispatch
The runtime flow: node starts → peer discovered → handshake → messages exchanged
The sync flow: WAL → WalPush → peer applies → WalAck
P2P does not replace offline-first sync — it complements it.
The core idea: Vix Sync preserves intent. Vix P2P moves that intent between nodes.