Skip to content

Architecture Overview

xNet is organized as a layered stack where each package builds on the ones below it. Lower packages never import from higher ones.

┌─────────────────────────────────────────┐
│ apps/electron · apps/web │ Applications
├─────────────────────────────────────────┤
│ @xnet/react │ React bindings
│ useQuery · useMutate · useNode │
│ useIdentity · XNetProvider │
├─────────────────────────────────────────┤
│ @xnet/data │ Data layer
│ defineSchema · NodeStore │
│ 16 property types · validation │
├─────────────────────────────────────────┤
│ @xnet/sync │ Sync primitives
│ Lamport clocks · Change<T> · chains │
│ Yjs security · envelopes · scoring │
├─────────────────────────────────────────┤
│ @xnet/storage │ Persistence
│ IndexedDB adapter │
├─────────────────────────────────────────┤
│ @xnet/identity │ Identity
│ DID:key · UCAN · key management │
├─────────────────────────────────────────┤
│ @xnet/crypto │ Cryptography
│ BLAKE3 · Ed25519 · XChaCha20 │
└─────────────────────────────────────────┘
PackagePurpose
@xnet/plugins4-layer plugin system (scripts, extensions, services, integrations)
@xnet/canvasInfinite canvas with spatial indexing
@xnet/editorTipTap rich text editor with Yjs collaboration
@xnet/networklibp2p node, WebRTC/WebSocket transport, peer security
@xnet/devtools7 debug panels for inspecting sync, store, schema, and more

Minimal coupling — Each package has a focused responsibility. The crypto package knows nothing about schemas. The data package knows nothing about React.

No default exports — Everything is a named export. Barrel files (index.ts) re-export from internal modules.

Factory functions — Classes are paired with factory functions (createFoo() alongside class Foo). This keeps the API surface consistent and makes testing easier.

Immutability — Core types like LamportTimestamp and Change<T> are treated as immutable. Functions return new objects instead of mutating in place.

Validation over exceptions — Functions return { valid: boolean, errors: [] } objects instead of throwing. Exceptions are reserved for programmer errors, not data validation.

flowchart LR
  A["User action"] --> B["useMutate()"]
  B --> C["NodeStore.update()"]
  C --> D["Change‹T› + Lamport"]
  D --> E["BLAKE3 hash"]
  E --> F["Ed25519 sign"]
  F --> G["Hash chain link"]
  G --> H["IndexedDB"]
  G --> I["SyncManager"]
flowchart LR
  A["useQuery(schema, filter)"] --> B["NodeStore.query()"]
  B --> C["IndexedDB read"]
  C --> D["FlatNode[]"]
  D --> E["Component render"]
  B -.->|subscribe| F["Change events"]
  F -.->|re-render| E
flowchart LR
  A["Y.Doc update"] --> B["YjsBatcher\n(2s window)"]
  B --> C["signYjsUpdate()"]
  C --> D["ConnectionManager\n.publish()"]
  D --> E["WebSocket"]
  E --> F["Signaling server"]
  F --> G["Peer WebSocket"]
  G --> H["Rate limit"]
  H --> I["Verify signature"]
  I --> J["Apply to Y.Doc"]