Peerkit is a peer-to-peer (P2P) data synchronization library. Its purpose is to enable developers to add decentralized data sharing to their applications, where groups of users have full control over their data and their participation.
Peerkit is not a runtime or an all-or-nothing framework. It is a library that can be integrated into existing applications. An app may use Peerkit to synchronize only a subset of its data, while the rest of the app uses conventional storage, APIs, or other backends. Peerkit only manages the data that is specified through its storage hooks. This makes adoption incremental: developers can add P2P capabilities to one feature of an existing app without rewriting the rest.
People lack agency over their digital tools. This manifests when:
Existing P2P frameworks have not solved this problem at scale, or provide mainly low-level features to this end.
Peerkit enables groups to credibly say "this is ours" about their digital tools. "Credibly" means a group realistically has agency: they can use, operate, and modify their tools with only a basic level of tech savvy.
For developers, choosing P2P should not be a painful choice. A developer new to Peerkit should be able to follow documentation and build a functioning app within hours.
Low-level libraries like libp2p provide networking primitives: transport, encryption, peer discovery, and connection management. iroh goes further — beyond its core connectivity layer (QUIC-based transport, key-based addressing, NAT traversal with relay fallback), it also offers composable protocols including gossip (iroh-gossip for pub-sub overlay networks), content-addressed blob transfer (iroh-blobs), an eventually-consistent key-value store (iroh-docs), and a Willow protocol implementation (iroh-willow, in development). Despite this broader feature set, both remain building blocks rather than application frameworks. A developer using libp2p or iroh directly must still solve:
Peerkit sits above these libraries and provides an opinionated-by-default but modular framework that addresses all of the above.
p2panda provides a similar layer of abstraction (structured data, gossip, access control) but is written in Rust, has limited documentation, unclear maturity, and its storage layer (SQLite) doesn't build to browser WASM. Peerkit differentiates by:
Peerkit is a set of packages composed by an orchestrator package. Three layers are implemented today:
@peerkit/transport-libp2p facade resolving to the -nodejs / -react-native builds): data transmission over the wire, specific to each target platform.@peerkit/transport-libp2p-core): the platform-independent transport contract — establishing connections, enforcing network access control, and routing messages, all behind an opaque NodeId.@peerkit/peerkit): persistent agent identity, the mapping between agent IDs and transport node IDs, the agent store, signed agent-info exchange, and a host for pluggable modules.Data synchronization capabilities are provided by modules registered for a node. The full-replication push and pull modules for authored data shipped today are one such example.
Higher-level concerns (structured data, indexing, state changes / CRDTs) build on these primitives but are speculative at this stage. They are sketched below as future direction.
Design principles:
An agent is a participant with a cryptographic identity: a public/private keypair. The agent's public key is its AgentId — a stable, verifiable identifier that does not depend on any central registry.
The framework generates the agent's keypair on first run and persists it through storage. On subsequent runs it reloads the same keypair. The app does not provide or manage key material. Signing happens and the private key is never exposed.
The local agent's public identity is accessible after initialization through the orchestrator.
AgentInfo is the shareable, serializable descriptor exchanged between peers. Its schema is defined in the API package. From the transport's perspective AgentInfo is opaque bytes. It carries the AgentId, the addresses where the agent can be contacted, an expiry timestamp, and a signature. The expiry timestamp demarcates a point in time after which the agent info should be discarded.
AgentId encoding: AgentId is the hex encoding of the raw 32-byte Ed25519 public key. This makes AgentId self-describing: any peer can reconstruct the public key from the string alone, with no key registry.
AgentInfo signature: every AgentInfo record carries a signature field — the Ed25519 signature over the canonical encoding of {agentId, addresses, expiresAt}. Verification is deterministic: decode the hex agentId to obtain the public key bytes and verify directly. No injected verifier or key lookup is required.
Signing: signed payloads (e.g. state changes in higher-level components) are signed by their author using the agent's private key. Peers verify the signature using the AgentId.
Storage is pluggable so developers can choose a backend that fits their app's constraints and deployment environment. Different components persist different things, so the framework defines several storage interfaces rather than one: IAgentStore for agent records, IAgentKeyStore for the local private key, and IAuthoredDataSyncStore as an example for synchronized data blobs.
Stores are injected as dependencies. Peerkit ships in-memory implementations as the working default (MemoryAgentStore, MemoryBlobStore), a persisted backend is planned. The in-memory blob store is one implementation of the data-store interface, and an app can supply its own. The exact interface for each store is documented at the package level.
Peerkit's network architecture distinguishes regular nodes — the participants — from infrastructure that supports them. Infrastructure serves two distinct roles:
NodeId. It is ignorant about agent identity and messages and just stores and passes through bytes. A relay can be given a persisted RelayCertificate, so its dial address stays stable across restarts, and can announce a public IP when it is bound behind NAT. It can also expose a liveness ping that is deliberately not gated by network access, so an external monitor can health-check the relay without holding access bytes.NodeId just like any other peer. When a node sends fresh agent info, the bootstrap node forwards it to the other peers it is connected to, so peers that joined after the original publish still learn about it.These two roles are logically independent. They can be hosted on the same machine at the same public address (as in the MVP, where a single infrastructure node serves both), or split across separate nodes.
Infrastructure is openly runnable: there is no privileged operator, no central authority. App developers, communities, or motivated users run infrastructure nodes; networks support multiple. Infrastructure has no privileged access to application data — it is gated by the network access control and routed end-to-end between regular nodes.
Resource management for relaying: A node acting as relay carries the bandwidth cost of forwarding traffic. The framework must:
Layer 0 is the layer of data transmission over the wire, specific to the target platform. Desktop, mobile and browser platforms have different requirements and permissions that allow or deny apps access to low-level networking features of the operating system. Also since Peerkit is written in TypeScript and executed in a JavaScript runtime, the various platforms impose more or less restrictions on the framework.
This has to be considered for each platform individually. Transports meeting the requirements of the target platform live on layer 0.
Layer 1 is responsible for establishing connections between peers, enforcing network access control, and routing application-level messages. The reference implementation is @peerkit/transport-libp2p-core, built on libp2p.
The transport's public API uses a single opaque NodeId type (a string) across every method, callback, and event. Library-specific identifiers (libp2p PeerId, libp2p Connection, multiformats Multiaddr) never cross the public boundary. The transport translates internally, so the networking library can be swapped without affecting any caller.
Mapping between peerkit AgentId (public key) and the transport's NodeId is the responsibility of the layer above the transport.
The only externally-supplied address in the transport's public API is the relay address used at bootstrap (passed at construction). All other addressing — peers' addresses, relay paths, identification of unconnected peers — is an implementation detail of the transport, handled through the networking library's own peer-discovery and peer-store mechanisms.
The transport package supports being run as part of the infrastructure or as a regular node. The two modes share access gating; they differ in their networking-library configuration: infrastructure nodes run a relay service, regular nodes use a relay client. Infrastructure-mode transports do not handle application messaging — peers attempting to send application data to infrastructure see a clean protocol-unsupported error. Relay mode adds the optional liveness ping and certificate persistence described under Infrastructure. Node mode runs a renewal timer that re-signs and re-sends the node's agent info before its TTL expires.
The mode is fixed at construction because the underlying networking library cannot reconfigure relay services at runtime.
The transport has three built-in protocols:
NetworkAccessBytes. Policy enforcement lives in the transport package.send. A call resolves once the transport has accepted the message and can take the next one, which applies backpressure to callers that await it; there is no delivery acknowledgement. Wire format owned by application code. Available on regular nodes only.Beyond these three, the transport opens arbitrary named protocol streams. A caller registers a handler with registerStreamHandler(protocol, …) and opens an outgoing stream with createStream(nodeId, protocol); each side gets an IStream for bidirectional bytes. The data synchronization modules ride on these custom streams rather than the application-message path. Incoming custom streams are subject to the same access check as everything else.
Concrete protocol identifiers and wire formats are documented in the transport package, not here.
Networks are closed by default. Every incoming connection must present NetworkAccessBytes in the access handshake before any other messages can be exchanged. The orchestrator registers a network access handler that returns accept or reject for given bytes.
To make a network open, the app explicitly registers a handler that accepts all bytes. This is opt-out, not the default.
The transport tracks two sets of agents per session:
A connection that fails the access check is fully closed.
Most consumer devices sit behind a router that blocks incoming connections. The networking library attempts direct connections where possible, and falls back to relay-assisted connectivity otherwise. When both peers are behind restrictive routers, a relay-assisted connection stays active as a fallback (estimated 5–15% of attempts). Infrastructure relays are required for relay-assisted connectivity; direct connections work without dedicated infrastructure when reachability allows.
At construction, a regular node receives one or more relay addresses (transport-internal config) and the NodeId of one or more bootstrap peers. Once connected to the relay, the node exchanges agent infos with the bootstrap peer and learns about existing peers. After the initial bootstrap, agent discovery proceeds bi-directionally with all connected peers.
Relay-mediated peer discovery and connection flow:
AgentInfo, which includes its relay-reachable address, and stores it in the agent store.AgentInfos to the relay.Local network discovery (mDNS) is a future enhancement that allows zero-configuration discovery on the same LAN.
A browser is a peer that uses different transports (post-MVP). Browsers cannot open listening sockets, so they can only initiate connections. Once connected, they participate as full regular nodes on those connections.
A network of only browser nodes cannot bootstrap. At least one infrastructure node must be reachable for initial connection.
| Platform | Capabilities |
|---|---|
| Desktop (Linux, macOS, Windows) | Full participation; can run as infrastructure (relay + bootstrap) or as a regular node. |
| Mobile (Android, iOS) | Regular node; background restrictions limit suitability for infrastructure. |
| Browser (post-MVP) | Lite regular node: cannot be bootstrapped to, always initiates connections. Can relay between peers it has connected to. |
All connections are encrypted.
The transport's contract to the rest of Peerkit is fully described by:
NodeId.NodeId for agent-info and application messages.A second implementation (iroh, future) would replace the library-specific internals while exposing the same surface. Packages above the transport remain unchanged when the networking library changes.
The transport is injected as a dependency. The orchestrator interacts with it through the ITransport interface, and the transport package exposes two construction paths — one for infrastructure nodes, one for regular nodes — both producing instances that satisfy ITransport. On infrastructure nodes, application messaging (send) is not serviced; peers attempting to use it see a clean protocol-unsupported error.
The vocabulary the boundary deals in:
NodeId — an opaque string identifying a peer at the transport level. Mapping to a peerkit AgentId is the caller's job.NodeAddress and RelayDialAddress — opaque dialable addresses, parsed by each transport implementation.NetworkAccessBytes — the byte sequence that proves access to a network has been granted.RelayCertificate — a caller-supplied TLS certificate for a relay's listener; persisting it keeps the relay's dial address stable across restarts.IStream — a bidirectional byte stream for arbitrary data, with message / remoteClose / close events.ITransport exposes: getNodeId; connect (to a list of dialable addresses); sendAgents and send (opaque agent-info and application bytes to a connected, access-granted peer); isConnected, getConnectedPeers, and isDirectConnection (connection state, where a non-direct connection is relayed); createStream and registerStreamHandler (custom protocol streams); disconnect; getListenAddresses (the transport's real dial addresses after startup, including any identity suffix); and shutDown. Construction takes the access handler plus the lifecycle callbacks (peer connected/disconnected, connected-to-relay, addresses-changed, agents-received, and the message handler on regular nodes).
The exact signatures, callback shapes, and TSDoc live in @peerkit/api and are published via typedoc; they are the authoritative reference and are not duplicated here. This boundary is what enables replacing js-libp2p with iroh or another networking library without affecting higher layers.
Peerkit core (@peerkit/peerkit) sits directly on the transport. It owns the agent's persistent identity, which survives transport restarts, maps that identity to transport node IDs, exchanges signed agent-info between peers, and hosts the modules that do the actual data work. It does not store or distribute application data itself.
Responsibilities:
AgentKeyPair) and its key lifecycle, loaded from or created in an injected IAgentKeyStore.AgentId and the transport's NodeId, in both directions.IAgentStore): the set of known, non-expired peers and their dial addresses.AgentInfo records.PeerkitNodeBuilder and PeerkitRelayBuilder.Operations between peers use the agent ID, not the transport's node ID. The mapping between the two lives in core. When a node learns of a peer from agent info, it has that peer's agent ID and dial addresses, so it can open an outbound connection and immediately associate the connection with the agent.
Inbound connections are different: at the transport level the remote is only a node ID, and the transport has no notion of agents. So the agent ID travels with the network access bytes in the access handshake. The transport treats those bytes as opaque; core reads the agent ID out of them and records the node-ID-to-agent-ID mapping. This is why agent identity is a core concern, not a transport one.
On top of this mapping, core lets consumers address peers by agent ID — sending a message, opening a stream, or checking connectivity — and translates to the underlying node ID.
Core exposes a small interface to modules so they can be packaged and distributed without depending on the @peerkit/peerkit package directly. A module (INodeModule) is attached to a node with register; the node calls init to wire it up, then start, and stop on shutdown. Through the node interface (IPeerkitNode) a module reads its own ownAgentId, lists connected agents, opens custom protocol streams (createStream), handles incoming ones (registerStreamHandler), and sends application messages — all keyed by agent ID rather than transport node ID. The exact signatures live in @peerkit/api and are published via typedoc.
Distributing application data is the job of modules, not of core. The full-replication push and pull modules shipped today are one example of how to do it — appropriate for small networks where every peer can hold everything, but not the right choice for every app. Other distribution policies (DHT, direct, topic-based) are possible against the same hook, and an app can attach its own modules instead.
These modules operate on authored data: each stored blob (StoredBlob) carries its content hash, the blob bytes, its author, and an authoredAt timestamp that is monotonic per author, so an author's blobs have a well-defined order. The store (IAuthoredDataSyncStore) owns the content-hash algorithm and that per-author clock. Local authoring goes through store (which hashes, stamps, persists, and notifies onAuthored subscribers); blobs received from peers go through accept, which the store may decline — for example when the distribution policy says this node should not hold the blob, or the blob is too large. The store also answers the queries the sync protocols need: by hash and author, the author's most recent blob, and the recent (authoredAt >= since) and historical (authoredAt < before) segments.
The distribution policy (IDataDistributionPolicy) decides which blobs a given peer should hold. Its one method, willStore(peerId, blobHash), receives the content hash — enough for hash-based routing such as a DHT — rather than the blob itself. FullReplicationPolicy is the default: willStore always returns true, so every peer keeps everything. Signatures for the store and the policy live in @peerkit/api.
Two modules use this together:
/peerkit/authored-data-push/v1): fire-and-forget. The module subscribes to onAuthored and streams newly authored blobs to connected peers, who run them through accept. No acknowledgement./peerkit/authored-data-pull/v1): periodic anti-entropy that repairs whatever push missed. Each exchange is segmented by an epoch boundary. The requester asks for two things: a recent delta (blobs with authoredAt >= epochStart, since the last one it has seen) and a historical segment (blobs before the epoch). The historical segment is reconciled by XOR-ing the hashes of the blobs each side holds: if the two XOR summaries match, the history is in sync and nothing is sent; if they differ, the responder sends its historical blobs. This keeps the common case — unchanged history — down to a single hash comparison instead of a full listing.Connection management is still pluggable in principle, but the shipped default simply maintains connections to all peers, which is adequate at the scale full replication targets.
Adds semantic meaning to blobs through schemas.
Capabilities:
API:
interface IStructuredData {
// Publish a schema definition so peers can decode blobs that reference it
publishSchema(schema: Schema): Promise<Hash>;
// Publish a blob whose structure conforms to the given schema
publishBlob(schemaId: Hash, data: Uint8Array): Promise<Hash>;
// New data coming in, matching a given schema
onNewData(schemaId: Hash, data: Uint8Array);
}
Adds queryability through deterministic metadata and distributed indexes. Layer 3 provides the "detail view" (fetch a specific blob by hash). Layer 4 provides the "list view" (browse and search blobs by their properties).
When a blob is created, metadata is deterministically extracted according to its schema's METADATA_FIELDS definition and published as separate blobs via publish(). METADATA_FIELDS is a list of field names declared in the schema that should be indexed — each named field gets its own metadata blob per value. The distribution strategy routes metadata blobs like any other blob. This means index lookups use the same get() mechanism as any other data retrieval — no special query protocol is needed.
Open question: the schema format is not yet specified. Either the schema definition format is hardcoded (e.g. a JSON Schema dialect with a
metadata_fieldskey), or an additional hook is needed (e.g.get_metadata_fields(schema) => string[]) to let schemas express this dynamically. This has implications for Layer 3 as well.
Metadata must be a separate blob because it needs to be routed independently of its source blob. In a DHT, hash("tag:meeting-notes") routes to different peers than the content blob itself — that's what makes index lookups efficient without scanning. If metadata were embedded in the source blob, it would be co-located with the content and distributed lookup would be impossible.
Example: A document blob has schema fields author, tags, and createdAt. The schema declares tags and author as metadata fields. When the blob is published:
publish(blob){ blobId, tag: "meeting-notes" } is published via publish(metadataBlob){ blobId, author: "agentXyz" } is published via publish(metadataBlob)To find all documents tagged "meeting-notes", a peer queries for metadata entries matching that tag and receives a list of blob IDs.
interface IIndexedData {
// Get all metadata entries for a blob
getMetadata(blobId: Hash): Promise<Metadata[]>;
// Find blobs matching field criteria (translates to Layer 2 lookups)
query(metadataQuery: MetadataQuery): Promise<Hash[]>;
// Verify that a metadata entry was correctly derived from the given blob
verifyMetadata(blob: Blob, metadata: Metadata): boolean;
}
Layers 2-4 deal with immutable blobs: you publish a blob, it gets distributed, it can be queried. But real applications need mutable state — a document that gets edited, a counter that increments, a list with items added and removed. In a P2P system without a central authority, multiple peers can mutate the same logical entity concurrently. Layer 5 defines how those concurrent mutations are expressed, linked, and resolved.
Open question: A CRDT definition needs to be declared somewhere — grouping a set of state change schemas that belong to the same CRDT, declaring the merge function, and optionally defining CRDT-level operations (e.g. deduplicating redundant state changes). The format and location of this definition is not yet specified.
State changes are blobs. They use Layer 3 schemas and Layer 4 indexing. No fundamentally new storage or networking mechanism is introduced — Layer 5 adds conventions and semantics on top of what exists below.
Each state change blob contains:
AgentId (public key) of the agent who made the changeCausal links form a DAG (directed acyclic graph) of state changes per entity. This DAG captures the order in which changes were made and allows detection of concurrent changes (branches in the DAG).
Peerkit ships one built-in CRDT: a CRUD object. The following are the state changes it allows:
Multiple peers may issue UpdateEntry or DeleteEntry for the same entity concurrently, creating branches in the causal DAG. Resolution is handled by merge functions.
When a peer receives state changes that form concurrent branches (no causal link between them), a merge is required. The CRDT schema defines how merges work.
Default merge strategy (last-writer-wins): Compare timestamps; if equal, compare blob hashes deterministically. Simple, sufficient for many apps, and requires no custom logic.
Custom merge functions: For richer semantics, the schema can specify a merge function. This is an open design question — options include:
The framework ships built-in merge strategies for common patterns. Custom strategies are possible but not required for typical CRUD apps.
Open question:
The merge logic could also be published as part of the CRDT specification. The CRDT spec would include the actual code and the intrepreter/vm/assembler it must be run with, i.e.
{
"merge": {
"interpreter": "node_22.1",
"exec": "function(a: AdditionStateChange, b: AdditionStateChange) -> AdditionState { return State { value: a.value + b.value }; }"
}
}
One of the benefits is it makes everything needed to use the CRDT fully in-band as long as the node has the interpreter available. There might need to be some tooling to make it easy for devs to write the code in a .js file and then have it imported into their CRDT type as a string.
Pruning in a P2P system without a central coordinator is inherently difficult, but possibly solvable with trade-offs.
Gossip treats a missing blob as something to repair. A pruned blob also looks missing. Without a mechanism to distinguish "not yet received" from "intentionally removed," gossip will undo prunes by re-fetching the data. Permanent tombstones solve this but grow monotonically, partially defeating the purpose of deletion.
Peerkit uses epochs — coordinated time windows that bound tombstone retention and define the sync protocol:
Snapshot-based anti-entropy: Peers periodically produce compact state snapshots. A returning or joining peer syncs against the snapshot. If a blob isn't in the snapshot, it doesn't exist — no tombstone needed. The snapshot is the authoritative representation of current state.
This bounds tombstone storage to one epoch's worth of deletions (proportional to deletion rate, not deletion history) and shifts the cost of long absence from the network to the returning peer (a one-time full resync) rather than burdening all peers with permanent tombstones.
When an entity is deleted:
Data is fully removed from the network over time — not just tombstoned. This is critical for storage scalability.
Update chain pruning is an open problem. Two structural issues make it difficult:
Chain integrity: If updates form a causal chain A → B → C, pruning an intermediate element (e.g. B) orphans all successors (C). The only structurally safe prune point is the tail — everything before the latest known update. But this is only well-defined once the merge has converged across all peers.
Consistency under degradation: Falling back to a different merge strategy when causal links are missing would cause peers with different data availability to reach different merge decisions — a consistency violation.
Open question: Should updates be a set rather than a chain? Set-based updates (no causal links between updates of the same entity) would make pruning straightforward: once the merge result is stable across peers, all inputs can be discarded. This trades causal ordering between updates for simpler pruning semantics.
For real-time collaborative editing (e.g. the knowledge base showcase app), the framework supports CRDT library integration. Instead of coarse-grained UpdateEntry state changes, an app can use fine-grained CRDT operations (e.g. Yjs operations for text editing).
These operations are still blobs, published and synced through Layers 2-4. The CRDT library handles merge semantics — the framework just transports the operations. This keeps Peerkit agnostic to the specific CRDT implementation while enabling rich collaborative features.
interface IStateChanges {
// Create a new entity; returns its ID (the hash of the CreateEntry blob)
createEntry(schemaId: Hash, data: unknown): Promise<Hash>;
// Update an entity; automatically includes causal links to known prior state changes
updateEntry(entityId: Hash, data: unknown): Promise<void>;
// Delete an entity and trigger pruning of its associated blobs
deleteEntry(entityId: Hash): Promise<void>;
// Get the current resolved state of an entity, applying merge if concurrent branches exist
getEntry(entityId: Hash): Promise<unknown | null>;
// Query entities via Layer 4 metadata
queryEntries(metadataQuery: MetadataQuery): Promise<Hash[]>;
// Get the full causal history (DAG of state changes) for an entity
getHistory(entityId: Hash): Promise<StateChange[]>;
}
Not yet specified. Potential areas:
Upgrading apps in a P2P network is fundamentally harder than in client-server: there is no central point to deploy updates to. Peers update independently, at different times, and the network will have peers running different versions simultaneously — potentially for a long time. Peerkit must make this easy and smooth.
Schema versioning: Schemas (Layer 3) carry a version number. When a schema evolves (new fields, changed structure), the new version is published as a new schema blob. Old schema blobs remain in the network. Peers on the new version must handle data created under old schemas.
Backward-compatible by default: Schema changes should be additive where possible — new fields with defaults, new optional metadata. Blobs created under an older schema version remain valid and readable. A peer running a newer app version can read old data without migration. A peer running an older app version that receives data with an unknown schema version can choose to store it opaquely (for gossiping to others) without processing it.
Validation across versions: Validation rules may change between versions. Each blob carries its schema version, and validation is applied according to the rules of that version. A v2 peer validates v1 blobs using v1 rules and v2 blobs using v2 rules. This avoids the problem of old peers rejecting new data or new peers rejecting old data.
Protocol versioning (Layer 0-1): The transport and networking protocols are versioned. Peers exchange version information during the connection handshake. The framework defines compatibility rules: which protocol versions can interoperate, and how to negotiate down to a common version.
No forced upgrades: In a P2P system where groups own their tools, upgrades cannot be forced. Peers must coexist across versions gracefully. The framework never requires all peers to be on the same version for the network to function. Degraded functionality (e.g. a new feature not available to old peers) is acceptable; broken connectivity is not.
Target: millions of nodes over time. Scalability is addressed at three levels:
Connections: Each peer maintains a bounded number of connections (O(log N) or a configured maximum). No operation requires contacting all peers.
Data distribution: Peerkit defines a pluggable distribution strategy interface but does not prescribe a routing topology. The distribution strategy is injected by the developer. Peerkit ships full replication as the built-in default. See Layer 2 for the interface and example strategies (DHT, direct replication, topic-based).
Destructive edits: This is critical for long-term storage scalability. Peerkit is not append-only at the protocol level. Data that has been superseded, deleted, or pruned can be fully removed over time. Without destructive edits, storage grows monotonically regardless of sharding, and networks eventually become unusable on constrained devices. Higher layers define the semantics of when pruning is appropriate. Tombstone storage is bounded through epoch-based compaction combined with snapshot-based anti-entropy for returning peers. See Layer 5's "Destructive edits and pruning" section for the full design.
Resource budgets (future): Each peer advertises its capacity (storage, bandwidth, connection count). The framework respects these limits and distributes load accordingly. Constrained devices (mobile, old hardware) take on less responsibility without being excluded from the network.
API discipline: No "get all" methods on potentially large collections. No networking concepts requiring notification of or connection to all peers. Every query and operation must be bounded.
Not every app needs every layer. The layers are additive — each builds on the one below but is independently useful. The MVP ships Layer 0 + 1 + 2 to get a working P2P library as quickly as possible.
What's included:
What's deliberately excluded from MVP:
The MVP gives developers: encrypted P2P connections, closed networks, and a shared blob store with full replication via gossip. That's enough for:
The app developer handles data structure, indexing, and conflict resolution in their own code. The developer experience improves as Layers 3-5 are added, but the MVP is functional.