W3C Draft Community Group Report
Latest published version: This document
Editor: [Editor Name]
This version: Draft, 5 April 2026
This specification defines a protocol for synchronising personal linked data graphs between multiple agents in a peer-to-peer manner. It defines the sync interface, diff format, conflict resolution semantics, peer discovery mechanism, and — critically — a pluggable sync module architecture that allows each shared graph to specify its own synchronisation strategy via a content-addressed WebAssembly module. The browser downloads, verifies, and executes the module in a capability-scoped sandbox. The module handles transport, merge logic, peer discovery, and governance validation. By standardising the synchronisation layer with pluggable strategies, this specification enables interoperable collaborative data applications without reliance on central servers while preserving sovereignty over sync semantics.
This document is a draft Community Group Report produced by the Personal Linked Data Community Group. It has not been reviewed or endorsed by the W3C Membership and is not a W3C Standard. This document is subject to change.
Comments on this specification are welcome. Please file issues on the GitHub repository.
- Introduction
- Conformance
- Terminology
- Data Model
- API
- Sync Modules
- GraphSyncModule Interface
- Module Capabilities
- Module Lifecycle
- Default Sync Module
- Wire Protocol (Default Module)
- Relay Protocol
- Peer Discovery
- NAT Traversal
- Merge Semantics (Default Module)
- Governance Integration
- Background Operation
- Publishing and Joining
- Signalling
- Security Considerations
- Privacy Considerations
- Examples
- References
The web's data model is fundamentally client-server: applications fetch data from centralised endpoints and write data back to them. This architecture creates single points of failure, imposes trust in server operators, and makes offline collaboration difficult or impossible.
Local-first software — in which data resides primarily on the user's device and is synchronised between peers — addresses these limitations. However, the web platform currently provides no native primitives for peer-to-peer data synchronisation. WebRTC enables media and data channels, but applications must build their own sync semantics on top of raw transport.
This specification defines a synchronisation protocol for linked data graphs: a standard interface and diff format that enables multiple agents to collaboratively maintain a shared, eventually-consistent semantic graph without requiring a central server.
Critically, this specification recognises that no single sync strategy is optimal for all use cases. A collaborative text editor requires different merge semantics than a social feed. A research dataset requires different peer discovery than a private messaging group. Rather than prescribing a single approach, this specification defines a pluggable sync module architecture: each shared graph specifies a WebAssembly module that implements the sync strategy. The browser downloads, verifies, sandboxes, and executes the module. The module handles transport, merge, peer discovery, governance validation, and all other sync-layer concerns.
This architecture provides:
- Sovereignty: Communities choose their own sync semantics — their module is the one component all peers must agree on.
- Evolvability: New sync strategies can be deployed without browser updates — modules are content-addressed code, not browser features.
- Safety: Modules run in a capability-scoped WASM sandbox with no access to DOM, filesystem, other graphs, or arbitrary network.
- Interoperability: All modules implement the same
GraphSyncModuleinterface, so the browser's graph API works identically regardless of the underlying sync strategy.
- Collaborative editing: Multiple users co-author a knowledge base, with changes propagating in real time as peers connect and disconnect.
- Peer-to-peer social: Social feeds, profiles, and interactions stored in shared graphs that participants sync directly — no platform intermediary.
- Distributed knowledge bases: Research groups, communities, or organisations maintain shared ontologies and datasets across institutional boundaries.
- Offline-first synchronisation: Field workers, travellers, or users on intermittent connections make local edits that automatically reconcile when connectivity resumes.
- Custom consensus protocols: Voting systems, multi-party computation, or domain-specific merge strategies implemented as sync modules without requiring browser changes.
- Governance-enforced collaboration: Communities define rules (who can contribute, how often, with what content) that are enforced at the sync layer by the module — not by the application UI.
This specification defines:
- The SharedGraph data model (extending Personal Linked Data Graphs [[PERSONAL-LINKED-DATA-GRAPHS]])
- The GraphDiff format for describing changes
- The GraphSyncModule WASM interface that sync modules MUST implement
- The capability-scoped sandbox in which sync modules execute
- The module lifecycle (installation, verification, update, removal, suspension)
- The default sync module that conforming user agents MUST ship
- The wire protocol and relay protocol for the default module
- The merge semantics (CRDT) for the default module
- Requirements for eventual consistency, causal ordering, and conflict resolution
- Governance integration via the module's
validate()method - A signalling mechanism for ephemeral peer communication outside the graph
- Background operation semantics for persistent sync across tab navigations
This specification does NOT define:
A specific transport protocol(the default module defines one; custom modules define their own)A specific CRDT or merge algorithm(the default module defines one; custom modules define their own)A specific peer discovery mechanism(the default module defines one; custom modules define their own)- The governance rule format (see [[GRAPH-GOVERNANCE]])
- Application-level schemas or ontologies
This specification depends on:
- [[PERSONAL-LINKED-DATA-GRAPHS]] — defines the PersonalGraph interface that SharedGraph extends
- [[DID-CORE]] — defines Decentralised Identifiers used for peer identity
- [[WEBASSEMBLY]] — defines the execution environment for sync modules
- [[WEBTRANSPORT]] — used by the default sync module for transport
- [[RFC2119]] — defines requirement level keywords
This specification is complemented by:
- [[GRAPH-GOVERNANCE]] — defines the governance constraint format and verification algorithms that the default sync module enforces
As well as sections marked as non-normative, all authoring guidelines, diagrams, examples, and notes in this specification are non-normative. Everything else in this specification is normative.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [[RFC2119]].
A conforming user agent MUST implement all normative requirements of this specification, including:
- The SharedGraph API (Section 5)
- The sync module sandbox (Section 8)
- The module lifecycle (Section 9)
- The default sync module (Section 10)
- Background operation (Section 17)
A conforming sync module MUST implement the GraphSyncModule interface defined in Section 7.
- SharedGraph
- A linked data graph that is synchronised between multiple peers. Extends PersonalGraph [[PERSONAL-LINKED-DATA-GRAPHS]] with sync capabilities. Identified by a URI of the form
graph://<relay>/<id>?module=<content-hash>. - Sync Module
- A content-addressed WebAssembly bundle that implements the
GraphSyncModuleinterface. The sync module handles transport, merge, peer discovery, and governance validation for a shared graph. All peers in a shared graph MUST run the same sync module. - GraphDiff
- A unit of change to a SharedGraph, consisting of additions, removals, a revision identifier, and a set of causal dependencies.
- Peer
- An agent participating in the synchronisation of a SharedGraph. Identified by a Decentralised Identifier (DID) [[DID-CORE]].
- Agent
- An entity (human user, automated process, or software agent) that controls a peer identity and interacts with SharedGraphs.
- Revision
- A content-addressed identifier for a GraphDiff, computed as a cryptographic hash of the diff's additions, removals, and causal dependencies.
- Causal Ordering
- A partial ordering of diffs such that a diff is applied only after all diffs it depends on have been applied.
- Content Hash
- A cryptographic hash (SHA-256 or equivalent) of a sync module's WASM binary. Used for content addressing, integrity verification, and ensuring all peers run identical code.
- Relay
- A server that facilitates message passing between peers. Relays forward messages but have no authority over graph data — they cannot modify, reject, or inspect diffs.
- Module Capability
- A scoped permission granted to a sync module by the browser's sandbox. Capabilities restrict what system resources (network, storage, cryptography) the module may access.
- Graph URI
- A URI of the form
graph://<relay-endpoints>/<graph-id>?module=<content-hash>that uniquely identifies a shared graph and encodes the information needed to join it: relay endpoint(s), graph identifier, and sync module hash.
A SharedGraph is a PersonalGraph [[PERSONAL-LINKED-DATA-GRAPHS]] extended with synchronisation capabilities. Each SharedGraph is identified by a globally unique Graph URI.
A SharedGraph MUST:
- Support all operations defined by PersonalGraph (add, remove, query triples)
- Maintain a set of known peers
- Track sync state
- Accept and produce GraphDiff objects
- Be associated with exactly one sync module
A SharedGraph MAY be backed by any storage mechanism that satisfies the PersonalGraph interface.
A GraphDiff represents an atomic unit of change to a SharedGraph. A GraphDiff consists of:
- additions: A set of signed semantic triples to be added to the graph. Each triple MUST include a cryptographic signature from the authoring agent.
- removals: A set of signed semantic triples to be removed from the graph. Each removal MUST be signed by an agent authorised to perform the removal.
- revision: A content-addressed identifier for this diff (see 4.3).
- dependencies: A set of revision identifiers representing the causal dependencies of this diff.
A GraphDiff MUST be treated as immutable once its revision identifier has been computed.
A Revision is a content-addressed identifier for a GraphDiff, computed as:
revision = hash(canonicalize(additions) || canonicalize(removals) || sort(dependencies))
The hash algorithm MUST be SHA-256 or a collision-resistant hash function of equivalent or greater strength.
The canonicalisation algorithm for triples MUST produce a deterministic byte representation regardless of insertion order. Implementations SHOULD use the RDF Dataset Canonicalization algorithm [[RDF-CANON]].
A Peer is an agent participating in the synchronisation of a SharedGraph. A single agent (identified by a DID) MAY have multiple concurrent sessions — for example, multiple browser tabs on the same device, or sessions across different devices. Each session is a distinct peer.
A peer is identified by the combination of:
- DID: The agent's Decentralised Identifier [[DID-CORE]], representing the user's identity.
- Session ID: A unique, randomly generated identifier for this specific session (tab, device, or context). The session ID MUST be generated using a cryptographically secure random source and MUST contain at least 128 bits of entropy.
Two peers with the same DID but different session IDs represent the same user on different tabs or devices. Two peers with different DIDs are different users.
A peer's DID MUST be resolvable to a DID Document containing at least one verification method suitable for digital signatures.
When a user opens a shared graph in a new tab or on a new device, the browser MUST generate a new session ID for that context. The session ID is ephemeral — it does not persist across page reloads or browser restarts.
The session ID enables:
- Targeted signalling: Send a signal to a specific tab or device, not just a user. For example, sending a WebRTC offer to the user's laptop session specifically, not their phone.
- Presence granularity: Show which devices a user is active on. "Alice is on her laptop and phone."
- Session handoff: A user can start a voice call on one device and transfer it to another by targeting the new session.
- Cursor/selection tracking: In collaborative editing, each tab has its own cursor position. The session ID distinguishes them.
If a sync connection is interrupted and re-established within the same browsing context (tab/window), the user agent SHOULD reuse the same session ID. If the browsing context is destroyed and recreated (e.g., page reload), a new session ID MUST be generated.
Peers MAY include an optional deviceLabel — a human-readable string identifying the device or context (e.g., "MacBook Pro", "iPhone", "Work Browser Tab 2"). This is provided by the user agent and is purely informational.
Two peers are the same peer if and only if both their DID and session ID are identical. Two peers with the same DID but different session IDs are the same user on different sessions. Implementations MUST treat them as distinct peers for the purposes of sync, signalling, and presence, but MAY group them for display purposes (e.g., showing "Alice (2 devices)" instead of two separate entries).
The sendSignal(remoteDid, payload) method targets ALL sessions of the specified DID. To target a specific session, use sendSignalToSession(remoteDid, sessionId, payload):
Promise<undefined> sendSignalToSession(USVString remoteDid, USVString sessionId, BufferSource payload);This is critical for WebRTC negotiation, where the offer must reach a specific device, and for session handoff scenarios.
A Graph URI uniquely identifies a shared graph and encodes the information required to join it. The URI scheme is graph:// with the following structure:
graph://<relay-endpoints>/<graph-id>?module=<content-hash>
Where:
- relay-endpoints: One or more comma-separated relay server hostnames. Example:
relay1.example.com,relay2.example.com - graph-id: A globally unique identifier for the graph, containing sufficient entropy to prevent guessing (RECOMMENDED: UUID v4 or 128+ bits of randomness).
- module: The content hash of the sync module's WASM binary. If omitted, the browser's default sync module is used.
Examples:
graph://relay.example.com/a3f8c2d1-7e9b-4f0a-8c6d-2e1f3a5b7d9e
graph://relay1.example.com,relay2.example.com/a3f8c2d1?module=sha256-abc123def456
The user agent MUST parse Graph URIs according to this scheme. If the URI cannot be parsed, the join() method MUST reject with a SyntaxError DOMException.
The sharing and joining of shared graphs is integrated into the navigator.graph namespace and the PersonalGraph interface. A personal graph becomes a SharedGraph by calling share() on it. Shared graphs are joined via navigator.graph.join().
[Exposed=Window, SecureContext]
partial interface PersonalGraphManager {
[NewObject] Promise<SharedGraph> join(USVString graphURI);
[NewObject] Promise<sequence<SharedGraphInfo>> listShared();
[NewObject] Promise<sequence<SyncModuleInfo>> listModules();
};
[Exposed=Window,Worker]
partial interface PersonalGraph {
[NewObject] Promise<SharedGraph> share(
optional SharedGraphOptions options = {}
);
};
dictionary SharedGraphOptions {
USVString module;
sequence<USVString> relays;
SharedGraphMetadata meta;
};
dictionary SharedGraphMetadata {
USVString name;
USVString description;
};
dictionary SharedGraphInfo {
USVString uri;
USVString name;
USVString moduleHash;
SyncState syncState;
unsigned long peerCount;
};
dictionary SyncModuleInfo {
USVString contentHash;
USVString? name;
unsigned long graphCount;
ModuleState state;
unsigned long long storageBytes;
};
enum ModuleState {
"running",
"suspended",
"error"
};The share() method on PersonalGraph MUST:
- If
options.moduleis specified:- Let moduleHash be the value of
options.module. - If the module identified by moduleHash is not installed, initiate the module installation flow (see Section 9.1).
- If the user denies installation, reject with a
NotAllowedErrorDOMException.
- Let moduleHash be the value of
- If
options.moduleis not specified, use the browser's default sync module. - Generate a globally unique graph identifier with at least 128 bits of entropy.
- Construct the Graph URI from the relay endpoints, graph identifier, and module hash.
- Initialise the sync module for this graph (call
init()). - Call
connect()on the module. - Return a SharedGraph object.
The listModules() method MUST return information about all installed sync modules, including their content hash, the number of graphs using them, their current state, and storage consumption.
The SharedGraph interface extends PersonalGraph with peer-to-peer synchronisation capabilities.
[Exposed=Window,Worker]
interface SharedGraph : PersonalGraph {
readonly attribute USVString uri;
readonly attribute USVString moduleHash;
readonly attribute SyncState syncState;
[NewObject] Promise<sequence<Peer>> peers();
[NewObject] Promise<sequence<Peer>> onlinePeers();
[NewObject] Promise<USVString> currentRevision();
Promise<undefined> sendSignal(USVString remoteDid, BufferSource payload);
Promise<undefined> sendSignalToSession(USVString remoteDid, USVString sessionId, BufferSource payload);
Promise<undefined> broadcast(BufferSource payload);
attribute EventHandler onpeerjoined;
attribute EventHandler onpeerleft;
attribute EventHandler onsyncstatechange;
attribute EventHandler onsignal;
attribute EventHandler ondiff;
};
dictionary Peer {
USVString did;
USVString sessionId;
USVString? publicKey;
USVString? deviceLabel;
DOMTimeStamp? lastSeen;
boolean online;
};[Exposed=Window,Worker]
interface GraphDiff {
readonly attribute USVString revision;
readonly attribute FrozenArray<SignedTriple> additions;
readonly attribute FrozenArray<SignedTriple> removals;
readonly attribute FrozenArray<USVString> dependencies;
readonly attribute USVString author;
readonly attribute DOMTimeStamp timestamp;
};
dictionary SignedTriple {
USVString source;
USVString predicate;
USVString target;
USVString signature;
USVString signer;
};enum SyncState {
"idle",
"connecting",
"syncing",
"synced",
"error"
};- "idle": The SharedGraph is not currently synchronising (e.g., no peers are connected, or the module is suspended).
- "connecting": The sync module is establishing connections to relay servers or peers.
- "syncing": The SharedGraph is actively exchanging diffs with peers.
- "synced": The SharedGraph has converged with all known peers and no pending diffs remain.
- "error": A sync error has occurred. The user agent SHOULD expose error details via a
SyncErrorEvent.
dictionary ValidationResult {
required boolean accepted;
USVString? module;
USVString? constraintId;
USVString? reason;
};The ValidationResult is returned by the sync module's validate() method. If accepted is false, the module, constraintId, and reason fields SHOULD be populated to identify the rejecting constraint and provide a human-readable explanation.
A sync module is a content-addressed WebAssembly bundle that implements the GraphSyncModule interface (Section 7). Each shared graph specifies its sync module via the module parameter in the Graph URI:
graph://relay.example.com/graph-id?module=sha256-<content-hash>
The sync module is the sovereignty boundary of a shared graph. It is the one component that all peers MUST agree on and execute. The module determines:
- Transport: How diffs are transmitted between peers (WebTransport, WebRTC, custom protocol)
- Merge strategy: How concurrent changes are reconciled (CRDT, OT, custom)
- Peer discovery: How peers find each other (relay-based, DHT, mDNS, custom)
- Governance validation: What rules govern who can contribute what (ZCAP, VC, custom)
All peers in a shared graph MUST run the same sync module, verified by content hash. A peer running a different module (different hash) is effectively participating in a different graph.
Sync modules are identified by the SHA-256 hash of their WASM binary:
content-hash = "sha256-" + hex(SHA-256(wasm-binary))
The user agent MUST verify the content hash of any downloaded module before installation. If the hash does not match, the module MUST be rejected and the installation MUST fail.
Content addressing provides:
- Integrity: The binary has not been tampered with.
- Identity: All peers verifiably run the same code.
- Cacheability: Modules with the same hash are identical and can be cached indefinitely.
Sync modules MAY be distributed via any content-addressable storage system, including but not limited to:
- HTTPS endpoints (e.g.,
https://modules.example.com/sha256-abc123.wasm) - IPFS / content-addressed networks
- Relay servers (modules can be requested from the relay specified in the Graph URI)
- Out-of-band transfer (copied manually)
The user agent SHOULD attempt to retrieve the module from the relay endpoint(s) specified in the Graph URI. The relay SHOULD serve module binaries at a well-known path:
https://<relay>/modules/<content-hash>.wasm
If the module cannot be retrieved, the join() method MUST reject with a NetworkError DOMException.
Sync modules execute in the browser process (not the renderer process). This means:
- Modules persist across tab navigations and browser restarts.
- Modules are not tied to any origin, page, or worker.
- Multiple pages from different origins can interact with the same shared graph through the same module instance.
The module runs inside a WebAssembly sandbox with capability-scoped permissions (see Section 8). The module has NO access to:
- The DOM or any renderer process state
- Other graphs or their data
- The filesystem
- Arbitrary network endpoints (only endpoints granted by capabilities)
- User data, cookies, local storage, or other browser storage
- Other sync modules
Installing a sync module is a privileged operation. The user agent MUST obtain explicit user consent before installing a new sync module. The consent flow SHOULD be analogous to Service Worker registration or extension installation:
- The user agent MUST display a prompt identifying:
- The content hash of the module
- The capabilities the module requests (see Section 8)
- The graph(s) that will use the module
- The relay endpoint(s)
- The user MUST explicitly approve ("Allow") or deny ("Deny") installation.
- If the user denies, the
join()orshare()call MUST reject with aNotAllowedErrorDOMException. - The user agent SHOULD remember the user's decision for subsequent encounters with the same module hash.
The user agent SHOULD provide a management interface for sync modules, analogous to "Manage Extensions" or "Manage Site Data". This interface SHOULD allow users to:
- View all installed sync modules with their content hash, name (if provided), and status
- See which shared graphs use each module
- View resource consumption (memory, network, storage) per module
- Pause and resume individual modules
- Remove modules (which disconnects from all graphs using that module)
- View the capabilities granted to each module
The graph URI SHOULD include at least two content-addressable locations for the sync module (e.g., IPFS CID and HTTPS URL). If the primary location is unavailable, the user agent MUST attempt alternate locations before reporting failure.
When a graph's sync module is updated (new content hash), existing peers MUST be notified via a MODULE_UPDATE wire protocol message. Peers MUST NOT apply the update until a quorum (>50% of known peers) has acknowledged availability of the new module. During transition, peers running the old module and peers running the new module MUST NOT exchange diffs.
The GraphSyncModule interface defines the contract that all sync modules MUST implement. The interface is defined in WebAssembly Interface Types (WIT) and exposed as WASM exports.
// This is the conceptual interface. The actual binding is via WASM exports.
// Each method corresponds to a named WASM export function.
interface GraphSyncModule {
// ── Lifecycle ──────────────────────────────────────────────
undefined init(ModuleConfig config);
undefined shutdown();
// ── Transport ──────────────────────────────────────────────
undefined connect(USVString graphUri, USVString localDid);
undefined disconnect();
// ── Sync ───────────────────────────────────────────────────
Revision commit(GraphDiff diff);
undefined onRemoteDiff(RemoteDiffCallback callback);
undefined requestSync(USVString fromRevision);
// ── Peer Management ────────────────────────────────────────
sequence<Peer> peers();
sequence<Peer> onlinePeers();
// ── Signalling (Ephemeral) ─────────────────────────────────
undefined sendSignal(USVString remoteDid, bytes payload);
undefined onSignal(SignalCallback callback);
// ── Governance Validation ──────────────────────────────────
ValidationResult validate(GraphDiff diff, USVString author, GraphReader graphState);
// ── Peer Discovery ─────────────────────────────────────────
sequence<Peer> discoverPeers(USVString graphUri);
};
callback RemoteDiffCallback = ValidationResult (GraphDiff diff);
callback SignalCallback = undefined (USVString remoteDid, bytes payload);dictionary ModuleConfig {
USVString graphUri;
USVString localDid;
GraphWriter graphWriter;
GraphReader graphReader;
CryptoProvider crypto;
NetworkProvider network;
unsigned long long maxMemoryBytes;
};The ModuleConfig is passed to init() and provides the module with capability handles for interacting with the browser's graph store, cryptographic key store, and network stack.
interface GraphReader {
sequence<SignedTriple> query(TripleQuery query);
USVString? resolveExpression(USVString address);
unsigned long long tripleCount();
USVString currentRevision();
};The GraphReader provides read-only access to the graph's current state. The sync module uses this for governance validation (inspecting the current graph to validate incoming diffs) and for computing sync state.
interface GraphWriter {
undefined applyDiff(GraphDiff diff);
undefined rejectDiff(GraphDiff diff, USVString reason);
};The GraphWriter provides write access to the graph. The sync module uses this to apply validated remote diffs to the local graph store.
interface CryptoProvider {
bytes sign(bytes data);
boolean verify(USVString did, bytes data, bytes signature);
USVString localDid();
bytes publicKey();
};The CryptoProvider grants scoped access to the browser's cryptographic key store. The module can request Ed25519 signatures using the local agent's key and verify signatures from other agents.
interface NetworkProvider {
WebTransportSession connectWebTransport(USVString url);
QUICConnection connectQUIC(USVString host, unsigned short port);
};The NetworkProvider grants scoped network access. The module can only establish connections using the protocols and endpoints permitted by its capabilities (see Section 8).
Called once when the module is first associated with a graph. The module MUST:
- Store the config for later use.
- Initialise any internal state (CRDT state, peer lists, message queues).
- NOT establish network connections (that happens in
connect()).
Called when the module is being removed or the graph is being left. The module MUST:
- Close all network connections.
- Flush any pending state to the graph writer.
- Release all resources.
Called to begin synchronisation. The module MUST:
- Parse the graph URI to extract relay endpoints and graph identifier.
- Establish transport connections to relay(s) or peers.
- Announce the local peer to the network.
- Begin receiving and processing remote diffs.
Called to pause or stop synchronisation. The module MUST:
- Announce departure to connected peers.
- Close all transport connections.
- Retain local state for potential reconnection.
Called when the local agent produces a new diff. The module MUST:
- Call
validate(diff, localDid, graphState)on the diff. If validation fails, the module MUST NOT distribute the diff and MUST return the rejection reason. - Assign a revision identifier to the diff.
- Apply the diff to the local graph via
graphWriter.applyDiff(diff). - Distribute the diff to connected peers via the transport.
- Return the revision.
Registers the callback that the browser invokes when the module receives a remote diff. The module calls this internally; the browser binds the callback during initialisation.
When the module receives a remote diff from the network, it MUST:
- Verify causal dependencies are satisfied. If not, buffer the diff.
- Call
validate(diff, author, graphState). If validation fails, discard the diff and do NOT propagate it. - Apply the diff via
graphWriter.applyDiff(diff). - Invoke the registered callback so the browser can dispatch events to pages.
- Forward the diff to other connected peers (gossip).
Called to request a full sync from peers starting from a given revision. Used for catch-up after reconnection or initial join.
The module MUST:
- Send a sync request to connected peers specifying the starting revision.
- Process the response diffs in causal order.
- Validate and apply each diff.
Called to validate a diff before it is applied or propagated. This is the governance enforcement point.
The module MUST:
- Verify the cryptographic signatures of all triples in the diff.
- Apply any governance rules defined by the module's implementation.
- Return a
ValidationResultindicating acceptance or rejection.
The graphState parameter provides read-only access to the current graph, enabling the module to inspect governance constraints, capability tokens, credentials, and other state needed for validation.
If validate() returns { accepted: false }, the diff MUST NOT be applied and MUST NOT be propagated to other peers.
Return the full set of known peers and the currently connected subset, respectively.
Send and receive ephemeral signals. Signals are NOT persisted in the graph, NOT included in diffs, and NOT subject to governance validation. They are transient messages for out-of-band coordination (cursor positions, typing indicators, WebRTC negotiation, etc.).
Actively discover peers for a graph. The module MAY use any mechanism: relay queries, DHT lookups, mDNS broadcasts, etc.
Sync modules run in a capability-scoped WASM sandbox. The browser grants a specific set of capabilities to each module at installation time. The module can only access system resources through these capabilities.
Capabilities are scoped to the module and graph, not global. A module installed for graph A cannot access resources granted to a module for graph B.
The following capabilities are defined:
| Capability | Description | Scope |
|---|---|---|
network:webtransport |
Establish WebTransport sessions to specified endpoints | Endpoints derived from Graph URI relay list |
network:quic |
Establish raw QUIC connections to specified endpoints | Endpoints derived from Graph URI relay list |
storage:graph-read |
Read triples from this graph's store | This graph only |
storage:graph-write |
Write diffs to this graph's store | This graph only |
crypto:sign |
Request Ed25519 signatures from the browser's key store | Local agent's key only |
crypto:verify |
Verify Ed25519 signatures | Any public key |
Sync modules MUST NOT have access to:
- The DOM or any renderer process state
- Other shared graphs or personal graphs
- The filesystem or origin-scoped storage (IndexedDB, localStorage, cookies)
- Arbitrary network endpoints not derived from the Graph URI
- Network protocols other than WebTransport and QUIC
- User data, browsing history, bookmarks, or extensions
- Other sync modules or their state
- System APIs (geolocation, camera, microphone, clipboard, etc.)
The user agent MUST enforce resource limits on sync modules:
| Resource | Limit | Enforcement |
|---|---|---|
| Memory | Configurable per module (default: 64 MB) | WASM linear memory limit; module terminated if exceeded |
| CPU | Configurable (default: 10% of one core) | Throttled; excess computation yields to other work |
| Network bandwidth | Configurable (default: 1 MB/s sustained) | Throttled; excess traffic queued |
| Storage | Bounded by graph storage quota | Module cannot allocate storage beyond graph's quota |
| Open connections | Maximum 16 simultaneous transport sessions | Excess connection attempts rejected |
The user agent SHOULD surface resource consumption in the Module Management UI (Section 6.6).
Sync modules SHOULD include a capability declaration in their WASM custom section (sync-module-meta), specifying:
{
"name": "Default Graph Sync",
"version": "1.0.0",
"capabilities": [
"network:webtransport",
"storage:graph-read",
"storage:graph-write",
"crypto:sign",
"crypto:verify"
],
"description": "CRDT-based sync via WebTransport relays"
}This metadata is informational — the browser enforces capabilities regardless of the declaration. However, the declaration enables the consent prompt (Section 6.5) to show the user what the module requests.
Module installation is triggered by:
graph.share({ module: "<content-hash>" })— when creating a new shared graph with a custom modulenavigator.graph.join("graph://...?module=<content-hash>")— when joining a graph that specifies a module
The installation algorithm:
- Let hash be the content hash from the Graph URI or share options.
- If a module with hash is already installed, skip to step 8.
- Attempt to download the module binary:
- For each relay endpoint in the Graph URI, attempt
GET https://<relay>/modules/<hash>.wasm. - If all relay attempts fail, attempt any configured module registries.
- If all attempts fail, reject with
NetworkError.
- For each relay endpoint in the Graph URI, attempt
- Compute
SHA-256(downloaded-binary)and verify it matches hash. - If the hash does not match, reject with
SecurityError. - Validate that the binary is a valid WebAssembly module with the required exports.
- Display the user consent prompt (Section 6.5). If denied, reject with
NotAllowedError. - Instantiate the module in the sandbox with appropriate capabilities.
- The module is now installed and ready for use.
The user agent MUST verify the content hash of every module:
- At download time (before installation)
- At load time (when loading from cache after browser restart)
- Periodically (RECOMMENDED: at least once per browser session)
If verification fails at any point, the user agent MUST:
- Immediately terminate the module.
- Disconnect all graphs using the module.
- Set the sync state of affected graphs to
"error". - Notify the user via the Module Management UI.
- Attempt to re-download and re-verify the module.
When a peer encounters a graph URI with a different module hash than the currently installed module, the user agent MUST:
- Treat this as a new module installation (Section 9.1).
- If the user approves the new module:
- Call
shutdown()on the old module instance for this graph. - Install and initialise the new module.
- Call
connect()on the new module. - Call
requestSync("genesis")to resynchronise from the beginning (since the new module may have different merge semantics).
- Call
- If the user denies the new module, the graph continues with the old module. Note that this may cause sync divergence with peers running the new module.
A module is removed when:
- The user explicitly removes it via the Module Management UI, OR
- All shared graphs using the module have been left
The removal algorithm:
- Call
shutdown()on the module instance. - Disconnect all graphs using this module.
- Delete the module binary from cache.
- Reclaim sandbox resources.
Graph data is NOT deleted when a module is removed. The SharedGraph's local data persists and remains accessible as a read-only PersonalGraph, consistent with the semantics defined in Section 18.3.
The user agent MAY suspend a module under resource pressure:
- Low battery conditions
- Metered network connections
- Memory pressure
- User-configured preferences
When suspending a module:
- Call
disconnect()on the module (allowing graceful connection teardown). - Serialise the module's WASM memory state to persistent storage.
- Set the sync state of affected graphs to
"idle". - Release sandbox resources.
When resuming:
- Restore the module's WASM memory state.
- Call
connect()to re-establish transport. - Call
requestSync(lastKnownRevision)to catch up. - Set the sync state of affected graphs to
"syncing".
A conforming user agent MUST ship with a built-in default sync module. The default module is used when:
graph.share()is called without specifying amoduleoption- A Graph URI omits the
moduleparameter
The default module MUST be available without download, user consent prompts, or network access. It is part of the browser, not a third-party module.
The default sync module implements:
| Concern | Strategy |
|---|---|
| Transport | WebTransport [[WEBTRANSPORT]] over QUIC to relay servers |
| Merge | Add-wins Observed-Remove Set (OR-Set) CRDT for triples |
| Peer discovery | Relay-based: peers connect to relay, relay groups by graph URI |
| Governance | Full governance engine per [[GRAPH-GOVERNANCE]]: ZCAP chain verification, VC credential checking, temporal constraints, content constraints |
| NAT traversal | Relay-mediated: all traffic flows through relay, works through any NAT configuration |
| Conflict resolution | Deterministic: add-wins for concurrent add/remove of same triple |
| Causal ordering | Revision dependency DAG |
The default sync module MUST satisfy all of the following:
- Implement the complete
GraphSyncModuleinterface (Section 7). - Guarantee eventual consistency: given the same set of diffs, all peers converge to the same graph state regardless of reception order.
- Enforce causal ordering: a diff is not applied until all its dependencies are satisfied.
- Implement the governance validation algorithms defined in [[GRAPH-GOVERNANCE]], including:
- Scope resolution (walking the entity hierarchy)
- ZCAP chain verification (delegation chains up to depth 10)
- Credential requirement checking
- Temporal constraint enforcement
- Content constraint enforcement
- Support the wire protocol defined in Section 11.
- Support the relay protocol defined in Section 12.
This section defines the wire protocol used by the default sync module. Custom sync modules are NOT required to use this protocol — they define their own.
All messages are serialised as CBOR [[RFC8949]] and transmitted over WebTransport streams.
The default module defines the following message types:
| Type Code | Name | Direction | Description |
|---|---|---|---|
0x01 |
DIFF |
Bidirectional | A new diff to be applied |
0x02 |
SYNC_REQ |
Client → Peer | Request diffs from a given revision |
0x03 |
SYNC_RESP |
Peer → Client | Response containing requested diffs |
0x04 |
SIGNAL |
Bidirectional | Ephemeral signal (not persisted) |
0x05 |
PEER_JOIN |
Bidirectional | Announce a new peer |
0x06 |
PEER_LEAVE |
Bidirectional | Announce a departing peer |
0x07 |
GOVERNANCE |
Bidirectional | Governance rule changes (propagated via sync like any diff, but typed for priority routing) |
DIFF {
type: 0x01,
revision: bytes(32), // SHA-256 hash
author: string, // DID of the diff author
timestamp: uint64, // Unix timestamp (milliseconds)
additions: [SignedTriple], // Array of signed triples to add
removals: [SignedTriple], // Array of signed triples to remove
dependencies: [bytes(32)] // Array of revision hashes this diff depends on
}
SignedTriple {
source: string, // Subject URI
predicate: string, // Predicate URI
target: string, // Object URI or literal
signature: bytes(64), // Ed25519 signature
signer: string // DID of the signer
}
SYNC_REQ {
type: 0x02,
fromRevision: bytes(32), // Request diffs after this revision
maxDiffs: uint32 // Maximum number of diffs to return (0 = no limit)
}
If fromRevision is all zeros (0x00 × 32), the request is for a full sync from genesis.
SYNC_RESP {
type: 0x03,
diffs: [DIFF], // Array of DIFF messages in causal order
hasMore: bool // Whether more diffs are available
}
SIGNAL {
type: 0x04,
senderDid: string, // DID of the sender
recipientDid: string, // DID of the recipient ("*" for broadcast)
payload: bytes // Arbitrary payload (max 64 KB)
}
PEER_JOIN {
type: 0x05,
did: string, // DID of the joining peer
sessionId: string, // Unique session identifier (tab/device)
publicKey: bytes(32), // Ed25519 public key
deviceLabel: string?, // Optional human-readable device label
timestamp: uint64 // Join timestamp
}
A single DID MAY have multiple concurrent PEER_JOIN messages with different session IDs. Each represents a distinct session (tab or device) for the same user.
PEER_LEAVE {
type: 0x06,
did: string, // DID of the departing peer
sessionId: string, // Session that is leaving
timestamp: uint64 // Leave timestamp
}
GOVERNANCE {
type: 0x07,
diff: DIFF // A DIFF containing governance constraint triples
}
Governance messages are structurally identical to DIFF messages but are typed separately so that relay servers and peers can prioritise their delivery. Governance diffs propagate via the same sync mechanism as content diffs but SHOULD be processed before content diffs when received simultaneously.
| Message Type | Maximum Size |
|---|---|
| DIFF | 1 MB |
| SYNC_REQ | 256 bytes |
| SYNC_RESP | 16 MB |
| SIGNAL | 64 KB |
| PEER_JOIN | 1 KB |
| PEER_LEAVE | 256 bytes |
| GOVERNANCE | 1 MB |
Messages exceeding these limits MUST be rejected by the receiver.
Each message is framed with a 4-byte big-endian length prefix followed by the CBOR-encoded message body:
[length: uint32-be][body: CBOR]
A relay server facilitates message passing between peers participating in the same shared graph. The relay protocol is intentionally simple — relays are dumb pipes, not authorities. Anyone can run a relay.
Peers connect to relay servers via WebTransport [[WEBTRANSPORT]] using the following URL scheme:
https://<relay-host>/graph/<graph-id>
Where <relay-host> is the relay hostname from the Graph URI and <graph-id> is the graph identifier.
Upon connection, the peer MUST send a PEER_JOIN message to identify itself. The relay MUST forward this message to all other peers connected to the same graph.
The relay operates as a message broker:
- When a peer sends a message (DIFF, SIGNAL, GOVERNANCE, etc.), the relay MUST forward it to all other peers connected to the same graph identifier.
- The relay MUST NOT modify message content.
- The relay MUST NOT inspect message content beyond the type code (needed for prioritisation).
- The relay MUST NOT reject messages based on content (it has no authority over graph data).
- The relay MAY prioritise GOVERNANCE messages over DIFF messages.
The relay groups connections by graph identifier:
- When a peer connects to
/graph/<id>, the relay adds the connection to the group for<id>. - Messages sent by any peer in the group are forwarded to all other peers in the group.
- When a peer disconnects, the relay MUST send a
PEER_LEAVEmessage to all remaining peers in the group. - Groups are created implicitly on first connection and destroyed when the last peer disconnects.
The relay MAY store recent diffs for catch-up purposes:
- When a relay stores diffs, it MUST respond to
SYNC_REQmessages from newly connecting peers. - The retention period is configurable by the relay operator. The relay SHOULD retain at least the most recent 1000 diffs or 24 hours of diffs, whichever is less.
- Stored diffs are served in causal order via
SYNC_RESPmessages. - The relay MUST NOT modify stored diffs.
Relay-side diff retention is OPTIONAL. Peers MUST NOT rely on relay retention for durability — the local graph store is the authoritative copy.
The relay has no authority over graph data:
- The relay cannot modify, reject, or filter diffs.
- The relay cannot read diff content (beyond the type code for routing).
- The relay cannot impersonate peers (peers authenticate via DID signatures).
- The relay cannot determine graph membership (it only knows which connections are grouped).
If a relay behaves maliciously (dropping messages, modifying content), peers can detect this through:
- Missing diffs (detected during sync catch-up with other peers)
- Invalid signatures (detected by receivers)
- Peer presence inconsistency (detected via direct peer-to-peer verification)
Peers SHOULD connect to multiple relays for resilience.
A Graph URI MAY specify multiple relay endpoints:
graph://relay1.example.com,relay2.example.com/graph-id?module=sha256-abc
The sync module SHOULD connect to all specified relays simultaneously. Messages are sent to all relays and deduplicated by revision hash on receipt. This provides:
- Resilience: If one relay goes down, sync continues via others.
- Censorship resistance: No single relay can block a peer.
- Performance: Peers discover each other faster.
A conforming relay MUST:
- Accept WebTransport connections at
https://<host>/graph/<id>. - Forward messages between peers in the same graph group.
- Send
PEER_LEAVEmessages when peers disconnect. - Serve module binaries at
https://<host>/modules/<hash>.wasm(if hosting modules).
A conforming relay SHOULD:
- Implement diff retention for catch-up.
- Rate-limit connections per IP and per graph to prevent abuse.
- Support TLS 1.3 for transport security.
- Log connection metadata (not message content) for operational purposes.
The relay protocol is simple enough that a minimal implementation requires only:
- A WebTransport server
- A map from graph ID to connected peer set
- Message forwarding logic
The default peer discovery mechanism is relay-based:
- The Graph URI encodes one or more relay endpoints.
- A peer connects to the relay(s) and sends
PEER_JOIN. - The relay forwards
PEER_JOINto all other connected peers. - Each peer maintains a local peer list based on
PEER_JOINandPEER_LEAVEmessages.
This mechanism requires no additional infrastructure. Any peer that can reach a relay can discover all other peers connected to the same graph.
Custom sync modules MAY implement DHT-based peer discovery for relay-less operation:
- The module publishes the local peer's DID and connection information to a distributed hash table, keyed by the graph identifier.
- Other peers query the DHT with the graph identifier to discover peers.
- Once peers are discovered, direct connections can be established.
The specification does NOT mandate a specific DHT implementation. Modules MAY use Kademlia, Chord, or any other DHT that satisfies their requirements.
Custom sync modules MAY implement mDNS-based peer discovery for local network synchronisation:
- The module broadcasts an mDNS service record advertising the graph identifier and the local peer's connection information.
- Other peers on the same local network discover the advertisement and establish direct connections.
- This enables zero-configuration sync on LANs without internet connectivity.
The sync module architecture allows arbitrary discovery mechanisms:
- QR code exchange (out-of-band URI sharing)
- Bluetooth Low Energy advertisements
- NFC tap-to-share
- DNS-based service discovery
- Social graph traversal
The specification does NOT constrain discovery mechanisms. Modules decide what works for their use case.
The default sync module uses relay-mediated NAT traversal:
- All traffic between peers flows through the relay server.
- Peers do not establish direct connections.
- This works through any NAT configuration (symmetric NAT, CGNAT, firewalls) because the peer only needs outbound connectivity to the relay.
This approach trades latency and bandwidth for universal connectivity. The relay adds a single hop to all traffic.
Custom sync modules MAY implement direct peer-to-peer connections with NAT traversal:
- ICE-like hole punching: The module uses the relay as a signalling channel to exchange connection candidates, then attempts direct QUIC connections through NAT.
- TURN-style relay fallback: If direct connection fails, the module falls back to relay-mediated traffic.
- Port mapping (UPnP/PCP): The module requests port mappings from the local NAT gateway.
The specification does NOT prescribe a specific NAT traversal strategy. The sync module decides based on its deployment context:
- Modules for mobile/constrained devices SHOULD use relay-mediated traffic (simpler, more reliable).
- Modules for desktop/server environments MAY implement direct connections (lower latency, less relay dependency).
- Modules for local-network-only use cases MAY skip NAT traversal entirely (mDNS discovery + direct LAN connections).
The default sync module uses an Add-wins Observed-Remove Set (OR-Set) CRDT for triples. This provides:
- Deterministic conflict resolution
- Commutative and associative merge
- Eventual consistency guarantee
- Tolerance of message reordering and duplication
Each triple has a unique identity computed as:
triple-id = SHA-256(source || predicate || target || author-did || timestamp)
This means the same (source, predicate, target) content authored by different agents or at different times produces different triple identities. This is intentional — it allows multiple agents to independently assert the same fact.
To add a triple:
- The agent signs the triple with their Ed25519 key.
- The signed triple is included in a GraphDiff's
additionsarray. - The triple is inserted into the OR-Set.
- The triple's identity is recorded in the set's add-set.
To remove a triple:
- The agent signs a removal for the triple with their Ed25519 key.
- The signed removal is included in a GraphDiff's
removalsarray. - The triple's identity is added to the set's remove-set (tombstone).
- The triple is marked as removed but NOT deleted from storage.
When concurrent (causally independent) operations produce both an add and a remove for the same triple identity:
- Add wins. The triple is present in the final state.
This is the OR-Set's defining property. It ensures that data is not accidentally lost due to concurrent operations. If removal is intended, the removing agent must re-issue the removal after observing the concurrent add.
Diffs are causally ordered via revision dependencies:
- Each diff declares its dependencies — the set of revision hashes it was produced "on top of".
- A diff MUST NOT be applied until all its dependencies have been applied.
- Dependencies form a Directed Acyclic Graph (DAG) of revisions.
- The DAG enables efficient sync: peers exchange missing revisions by traversing the DAG from their last known common point.
The OR-Set CRDT guarantees that:
Given any two peers that have received the same set of diffs (regardless of order), their graph states are identical.
Proof sketch: The OR-Set merge function is commutative, associative, and idempotent. Applying the same set of add/remove operations in any order produces the same result. Causal ordering ensures that dependency relationships are respected, but the CRDT converges regardless of operation order within a causal generation.
Tombstones (remove-set entries) accumulate over time. The default module SHOULD implement tombstone garbage collection:
- A tombstone MAY be garbage-collected after all peers have acknowledged the revision containing the removal.
- A tombstone MUST NOT be garbage-collected if any peer may not yet have received it (this would cause the removed triple to reappear).
- Implementations SHOULD track peer sync state to determine when garbage collection is safe.
- As a conservative default, tombstones SHOULD be retained for at least 30 days.
When multiple concurrent operations set different values for the same scalar property (a predicate with maxCount=1 in the shape definition), the OR-Set retains ALL values. Implementations MUST resolve scalar conflicts using a deterministic last-writer-wins (LWW) strategy: the triple with the lexicographically greatest (timestamp, author-DID) pair wins. The losing triple is tombstoned.
Implementations SHOULD garbage-collect tombstones after all known peers have acknowledged the removal (i.e., all peers' current revision includes the removal diff's revision as an ancestor). Implementations MUST NOT garbage-collect tombstones while any known peer has not yet synced past the removal. Tombstones older than 30 days with no unsynced peers MAY be collected.
Implementations MAY prune the revision DAG by compacting contiguous sequences of revisions into a single checkpoint revision, provided all peers have synced past the compacted range.
The sync module's validate() method is the governance enforcement point for a shared graph. It is the one place where rules are checked, and it runs identically on all peers. This makes the sync module the sovereignty boundary — the component that defines what is and is not permitted in the graph.
When a diff arrives (either from the local agent or from a remote peer), the following validation flow occurs:
- The sync module receives the diff.
- The module calls
validate(diff, author, graphState). - The
validate()method receives:- diff: The GraphDiff containing additions and removals.
- author: The DID of the agent who produced the diff.
- graphState: A
GraphReaderproviding read-only access to the current graph state.
- The module inspects the graph state for governance constraints (e.g.,
governance://predicates per [[GRAPH-GOVERNANCE]]). - The module evaluates each triple in the diff against applicable constraints.
- If all triples pass validation,
validate()returns{ accepted: true }. - If any triple fails validation,
validate()returns{ accepted: false, module: "...", constraintId: "...", reason: "..." }.
When a diff is rejected:
- The diff MUST NOT be applied to the local graph.
- The diff MUST NOT be forwarded to other peers.
- If the diff was produced locally, the
commit()method MUST return the rejection reason. - If the diff was received remotely, the module SHOULD log the rejection for debugging.
The default sync module implements the full governance specification defined in [[GRAPH-GOVERNANCE]]. This includes:
- Scope resolution: Walking the entity hierarchy to determine which constraints apply to a given triple (ancestry chain, scope inheritance, precedence rules).
- Capability verification (ZCAP): Verifying that the diff author holds a valid Authorization Capability chain for the triple's predicate and scope.
- Credential verification (VC): Checking that the diff author holds required Verifiable Credentials.
- Temporal verification: Enforcing rate limits (minimum intervals, maximum counts per window).
- Content verification: Validating triple targets against content constraints (length limits, blocked patterns, URL policies, media type restrictions).
The default module evaluates constraints in the order listed above (cheapest first) and stops at the first rejection.
Custom sync modules implement whatever governance logic they want. A module MAY:
- Implement a subset of [[GRAPH-GOVERNANCE]] (e.g., only ZCAP, no content constraints).
- Implement entirely different governance models (voting, reputation, proof-of-work, etc.).
- Implement no governance at all (permissive — any signed diff is accepted).
- Implement governance models that don't yet exist.
The specification does NOT constrain governance implementations. The sync module is sovereign.
Governance rules are graph data — triples with governance:// predicates stored in the same graph they govern. Changes to governance rules propagate via the same sync protocol as content changes:
- An authorised agent adds or removes governance triples.
- The triples are included in a GraphDiff.
- The diff is validated and distributed.
- All peers receive the governance changes and enforce them.
The default module types governance diffs as GOVERNANCE messages (Section 11.2) for priority routing, but structurally they are ordinary diffs.
Because all peers run the same sync module (verified by content hash):
- The same diff evaluated against the same graph state produces the same validation result on every peer.
- A triple rejected by one honest peer will be rejected by all honest peers.
- No application, UI, or agent can bypass governance — the sync module is the enforcement point, and it runs below the application layer.
This is the fundamental security property of the architecture. The application layer is cosmetic. The sync module is authoritative.
Sync modules run in the browser process, NOT in page or worker context. This provides:
- Persistence: Modules continue running when tabs are closed, navigated, or the user switches to a different application.
- Independence: No origin or page owns the sync module. It serves all pages that access the graph.
- Background sync: Incoming diffs are applied to the graph store in the background. When a page opens a graph, data is already current.
The user agent SHOULD maintain WebTransport connections to relay servers even when no tabs or pages are accessing a shared graph. This enables:
- Real-time sync in the background
- Instant data availability when a page opens a graph
- Push-style updates without polling
The user agent SHOULD throttle background sync under resource constraints:
| Condition | Throttling |
|---|---|
| Battery below 20% | Reduce sync frequency to every 5 minutes |
| Battery below 10% | Suspend all sync modules; resume on charge |
| Metered network | Reduce sync frequency; defer large diffs |
| Memory pressure | Suspend least-recently-used modules |
| User preference "Low Data Mode" | Sync on explicit request only |
The user agent SHOULD provide UI showing sync status per graph:
- Graph name and URI
- Current sync state (idle, connecting, syncing, synced, error)
- Number of online peers
- Last sync timestamp
- Bandwidth consumed
- Pending diffs (outgoing changes not yet acknowledged)
This UI SHOULD be accessible from the browser's settings or toolbar, analogous to download manager or notification settings.
Sync events MUST be deliverable to Service Workers registered for the origin that created or joined the graph.
When a GraphDiff is received while no documents are open, the user agent MUST dispatch a SyncEvent to the active Service Worker, enabling offline processing of incoming changes.
[Exposed=ServiceWorker]
interface SyncEvent : ExtendableEvent {
readonly attribute USVString sharedGraphURI;
readonly attribute GraphDiff diff;
};The user agent MAY implement wake-on-diff for suspended modules:
- The relay server sends a lightweight push notification (e.g., Web Push) when a new diff is available for a graph.
- The user agent wakes the relevant sync module.
- The module connects, syncs, and processes the diff.
- The module returns to suspended state.
This enables battery-efficient background sync without persistent connections.
Publishing converts a PersonalGraph into a SharedGraph by associating it with a sync module and making it discoverable by peers.
The share() method on PersonalGraph MUST:
- Determine the sync module:
- If
options.moduleis specified, use that module (install if necessary per Section 9.1). - If
options.moduleis not specified, use the default sync module.
- If
- Determine relay endpoints:
- If
options.relaysis specified, use those relays. - If
options.relaysis not specified, the user agent SHOULD use a default relay. Implementations MAY operate their own default relays or prompt the user.
- If
- Generate a globally unique graph identifier with at least 128 bits of entropy.
- Construct the Graph URI:
graph://<relays>/<id>?module=<hash>. - Call
init()on the sync module with aModuleConfigcontaining the graph URI and local DID. - Call
connect()on the sync module. - Return a SharedGraph object that reflects the current state of the underlying PersonalGraph.
Joining connects an agent to an existing SharedGraph and begins synchronisation.
The join() method on navigator.graph MUST:
- Parse the Graph URI to extract relay endpoints, graph identifier, and module hash.
- If a module hash is specified:
- If the module is already installed, use it.
- If the module is not installed, initiate installation (Section 9.1).
- If installation fails or the user denies it, reject with
NotAllowedError.
- If no module hash is specified, use the default sync module.
- Create a new local graph store for this shared graph.
- Call
init()on the sync module. - Call
connect()on the sync module. - Call
requestSync("genesis")to perform initial synchronisation. - Return a SharedGraph object.
The user agent SHOULD display a consent prompt before joining, informing the user:
- The graph URI and relay endpoint(s)
- The sync module being used
- That their DID will be visible to other peers
- Estimated storage requirements
Leaving disconnects an agent from a SharedGraph.
The leave() method MUST:
- Call
disconnect()on the sync module. - Cease all sync activity for this graph.
- If the
retainLocalCopyoption istrue(the default), the local graph data MUST be preserved and accessible as a read-only PersonalGraph. - If the
retainLocalCopyoption isfalse, the local graph data MAY be deleted. - If no other graphs use the same sync module, the module MAY be removed (Section 9.4).
The sendSignal(did, payload) method sends arbitrary data to a specific peer identified by their DID.
Promise<undefined> sendSignal(USVString remoteDid, BufferSource payload);The payload is an arbitrary byte sequence (maximum 64 KB). The signal is delivered on a best-effort basis — delivery is NOT guaranteed if the target peer is offline.
Signals are intended for out-of-band coordination such as:
- Cursor position sharing
- Typing indicators
- WebRTC negotiation
- Custom protocol handshakes
- Application-level messaging that does not belong in the graph
The sync module's sendSignal() method is called to transmit the signal via the module's transport.
The broadcast(payload) method sends arbitrary data to all currently connected peers.
Promise<undefined> broadcast(BufferSource payload);The same delivery semantics as sendSignal apply. The broadcast is sent to all peers known to be online at the time of the call.
Signals are ephemeral. They MUST NOT be persisted in the graph, included in GraphDiffs, or replayed during sync. A signal exists only as a transient message between peers.
Signals are NOT subject to governance validation. They bypass the validate() method entirely. This is intentional — signals are for coordination, not data.
Receiving peers MUST dispatch a SignalEvent to the SharedGraph:
[Exposed=Window,Worker]
interface SignalEvent : Event {
readonly attribute USVString senderDid;
readonly attribute ArrayBuffer payload;
};All triples within a GraphDiff — both additions and removals — MUST include a cryptographic signature from the authoring agent. This provides authentication: peers can verify that a triple was authored by the agent whose DID is associated with the signature.
The default signature algorithm is Ed25519 over SHA-256. The signing input is:
sign-input = SHA-256(source || predicate || target || timestamp)
A conforming sync module MUST verify the signature of every triple in a received GraphDiff before applying it. Triples with invalid or missing signatures MUST be rejected.
Peers are identified by DIDs [[DID-CORE]]. Implementations MUST verify that a peer's claimed DID corresponds to the key material used for signing triples and establishing connections. This prevents peer impersonation.
Sync module code is content-addressed. The SHA-256 hash of the WASM binary is verified before execution and periodically thereafter. This ensures:
- The module has not been tampered with after download.
- All peers verifiably run the same code.
- Cached modules can be verified without re-downloading.
Sync modules run in a WASM sandbox with capability-scoped permissions (Section 8). The sandbox provides:
- Memory isolation: The module cannot read or write memory outside its WASM linear memory.
- Network isolation: The module can only access network endpoints granted by capabilities.
- Storage isolation: The module can only access the graph it is associated with.
- No DOM access: The module cannot manipulate the renderer or page content.
A malicious sync module could:
| Threat | Mitigation |
|---|---|
| Consume excessive resources (CPU, memory, network) | Browser enforces resource limits (Section 8.4); user can suspend/remove via Module Management UI |
| Produce invalid diffs (corrupt data) | Other peers' modules validate incoming diffs; invalid diffs are rejected |
| Leak graph data to the relay or external parties | Relay transport uses TLS; module cannot access endpoints outside its capabilities; E2E encryption can be layered above the module |
| Accept diffs that should be rejected (weak governance) | All peers must agree on the module (content hash); a module with weak governance is a community choice, not a browser vulnerability |
| Deny-of-service via slow validation | Browser enforces timeout on validate() calls (RECOMMENDED: 5 seconds); module terminated if exceeded |
All peers in a shared graph MUST run the same sync module, verified by the content hash in the Graph URI. A peer running a different module (different hash) is not part of the same graph.
When a module update occurs (new hash):
- The new hash constitutes a new graph configuration.
- Peers must migrate to the new module to continue participating.
- The browser handles this via the update flow (Section 9.3).
Sync modules SHOULD implement mitigations against denial-of-service attacks via diff flooding, including:
- Rate limiting incoming diffs per peer
- Maximum diff size limits (Section 11.4)
- Banning peers that repeatedly submit invalid diffs
- Relay-side connection rate limiting (Section 12.8)
SharedGraph URIs SHOULD contain sufficient entropy (128+ bits) in the graph identifier to prevent unauthorised join attempts via guessing. Knowledge of a SharedGraph URI constitutes the minimum requirement for joining — additional access control is handled by the sync module's governance validation.
Relays are untrusted intermediaries:
- Relays cannot modify diff content (signatures verify integrity).
- Relays cannot forge diffs (they don't have agents' private keys).
- Relays can drop messages (detected by sync catch-up with other peers).
- Relays can observe connection metadata (who connects to what graph, when).
For metadata privacy, peers MAY:
- Connect through Tor or VPN.
- Use multiple relays and rotate between them.
- Implement relay-blinding techniques in custom modules.
Peers in a SharedGraph are identified by their DIDs. All peers can see the DIDs of all other peers. This constitutes identity disclosure — agents participating in a SharedGraph reveal their decentralised identity to all other participants.
Users MUST be informed when joining a SharedGraph that their DID will be visible to other peers. User agents SHOULD provide a clear consent prompt (Section 18.2).
By default, all graph content is visible to all peers. There is no built-in encryption of triple content.
Implementations MAY layer end-to-end encryption (E2EE) over the sync protocol. When E2EE is applied:
- Triple payloads SHOULD be encrypted before being included in a GraphDiff.
- Key management is the responsibility of the E2EE layer, not this specification.
- Custom sync modules MAY implement E2EE natively.
Even with E2EE, metadata such as the number of triples, diff frequency, peer connection times, and graph URI are observable by:
- Relay servers (connection metadata)
- Other peers (diff metadata)
- Network intermediaries (connection patterns)
Relay servers observe:
- Which DIDs connect to which graphs
- Connection timestamps and durations
- Message sizes and frequencies
- IP addresses of connecting peers
Relay operators SHOULD minimise metadata retention. Relay operators SHOULD publish a privacy policy.
Relay operators CANNOT observe diff content (encrypted by TLS). For sensitive use cases, implementations SHOULD use anonymization techniques (e.g., connecting via Tor, using ephemeral DIDs for relay authentication).
Graph URIs SHOULD specify at least two relay endpoints for redundancy. If all specified relays are unavailable, the user agent MUST enter a "disconnected" sync state and attempt reconnection with exponential backoff.
NOTE: The economic model for relay operation is out of scope for this specification. Communities MAY operate their own relays, use public relays, or incentivize relay operation through application-layer mechanisms.
The content hash of a sync module reveals what type of graph a peer is participating in. If a module is unique to a specific community or application, the module hash alone can identify the community. Users should be aware that the module hash in a Graph URI is not secret.
SharedGraph data stored locally by the user agent SHOULD be protected with the same security measures as other browser storage (e.g., IndexedDB). User agents MUST delete SharedGraph data when the user clears site data, unless the graph has been explicitly marked for retention via the Module Management UI.
This section is non-normative.
// Create a personal graph
const graph = await navigator.graph.create("project-notes");
// Add some initial data
await graph.addTriple({
source: "note:1",
predicate: "schema:name",
target: "Meeting Notes — April 2026"
});
// Share it using the default sync module
const shared = await graph.share({
meta: { name: "Project Notes", description: "Shared notes for the team" }
});
console.log("SharedGraph URI:", shared.uri);
// → "graph://default-relay.browser.example/a3f8c2d1-7e9b-4f0a-..."
// No ?module= parameter — default module is impliedconst graph = await navigator.graph.create("voting-system");
// Share with a custom module that implements quadratic voting
const shared = await graph.share({
module: "sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
relays: ["relay1.example.com", "relay2.example.com"],
meta: { name: "Community Votes" }
});
console.log("SharedGraph URI:", shared.uri);
// → "graph://relay1.example.com,relay2.example.com/b7d9e2f1-...?module=sha256-e3b0c44..."// Join using a URI received out-of-band (e.g., shared via link)
// Browser prompts: "Install sync module sha256-abc123? [Allow/Deny]"
const shared = await navigator.graph.join(
"graph://relay.example.com/a3f8c2d1-...?module=sha256-abc123"
);
// Listen for sync state changes
shared.onsyncstatechange = (event) => {
console.log("Sync state:", shared.syncState);
};
// Query the graph (standard PersonalGraph API)
const results = await shared.query("SELECT ?s ?p ?o WHERE { ?s ?p ?o }");
console.log("Triples:", results.length);
// See who else is here
const peers = await shared.onlinePeers();
console.log("Online peers:", peers.map(p => p.did));const shared = await navigator.graph.join(graphURI);
// React to incoming changes from peers
shared.addEventListener("diff", (event) => {
const diff = event.diff;
console.log(`Revision ${diff.revision} from ${diff.author}:`);
console.log(` +${diff.additions.length} triples`);
console.log(` -${diff.removals.length} triples`);
// Update UI based on the changes
for (const triple of diff.additions) {
if (triple.predicate === "schema:name") {
updateTitleInUI(triple.source, triple.target);
}
}
});const shared = await navigator.graph.join(graphURI);
// Listen for signals from other peers
shared.onsignal = (event) => {
const payload = new TextDecoder().decode(event.payload);
const data = JSON.parse(payload);
if (data.type === "cursor-position") {
showRemoteCursor(event.senderDid, data.x, data.y);
}
};
// Broadcast cursor position to all peers (ephemeral, not stored)
document.addEventListener("mousemove", (e) => {
const payload = new TextEncoder().encode(JSON.stringify({
type: "cursor-position",
x: e.clientX,
y: e.clientY
}));
shared.broadcast(payload);
});// List all installed modules
const modules = await navigator.graph.listModules();
for (const mod of modules) {
console.log(`Module ${mod.contentHash}:`);
console.log(` Name: ${mod.name}`);
console.log(` Graphs: ${mod.graphCount}`);
console.log(` State: ${mod.state}`);
console.log(` Storage: ${mod.storageBytes} bytes`);
}
// List all shared graphs
const graphs = await navigator.graph.listShared();
for (const g of graphs) {
console.log(`Graph ${g.uri}:`);
console.log(` Name: ${g.name}`);
console.log(` Module: ${g.moduleHash}`);
console.log(` Peers: ${g.peerCount}`);
console.log(` State: ${g.syncState}`);
}const shared = await navigator.graph.join(graphURI);
// Check if a triple would be allowed before attempting to add it
const result = await shared.canAddTriple({
source: "msg:123",
predicate: "app:body",
target: "Hello, world!"
});
if (result.allowed) {
await shared.addTriple({ source: "msg:123", predicate: "app:body", target: "Hello, world!" });
} else {
console.log(`Blocked by ${result.module}: ${result.reason}`);
// e.g., "Blocked by temporal: Rate limit: wait 20 more seconds"
}- [DID-CORE]
- Decentralized Identifiers (DIDs) v1.0. W3C Recommendation.
- [PERSONAL-LINKED-DATA-GRAPHS]
- Personal Linked Data Graphs. Draft. (Companion specification)
- [GRAPH-GOVERNANCE]
- Graph Governance: Constraint Enforcement for Shared Linked Data Graphs. Draft. (Companion specification)
- [RDF-CANON]
- RDF Dataset Canonicalization. W3C Recommendation.
- [RFC2119]
- Key words for use in RFCs to Indicate Requirement Levels. IETF RFC 2119.
- [RFC8949]
- Concise Binary Object Representation (CBOR). IETF RFC 8949.
- [WEBASSEMBLY]
- WebAssembly Core Specification. W3C Recommendation.
- [WEBTRANSPORT]
- WebTransport. W3C Working Draft.
- [WEBRTC]
- WebRTC: Real-Time Communication in Browsers. W3C Recommendation.
- [CRDT]
- Shapiro, M. et al. "Conflict-free Replicated Data Types." SSS 2011.
- [LOCAL-FIRST]
- Kleppmann, M. et al. "Local-first software: you own your data, in spite of the cloud." Onward! 2019.
- [SOLID]
- Solid Protocol. W3C Solid Community Group.
- [ZCAP-LD]
- Authorization Capabilities for Linked Data. W3C Community Group Report.
- [VC-DATA-MODEL-2.0]
- Verifiable Credentials Data Model v2.0. W3C Recommendation.