Data Model
Overview
Section titled “Overview”xNet’s data model has three layers:
- Schemas — Define the shape of your data (property types, defaults, validation)
- Nodes — Individual data records conforming to a schema
- Changes — Signed, hash-chained mutations that form an audit trail
Schemas
Section titled “Schemas”A schema declares the properties a node can have, using typed builder functions:
import { defineSchema, text, number, boolean, select, relation } from '@xnet/data'
const Task = defineSchema('task', { title: text(), priority: number({ default: 0 }), done: boolean({ default: false }), status: select({ options: ['todo', 'doing', 'done'] }), project: relation()})Each builder returns a typed descriptor that encodes the value type, default, and cardinality. TypeScript infers the full type from the schema definition — no manual type annotations needed.
xNet provides 16 property types:
| Type | Value | Notes |
|---|---|---|
text() | string | Plain text |
number() | number | Numeric value |
boolean() | boolean | True/false |
date() | number | Unix timestamp |
select() | string | Single choice from options |
multiSelect() | string[] | Multiple choices |
url() | string | URL string |
email() | string | Email address |
phone() | string | Phone number |
relation() | string | Reference to another node |
relation.many() | string[] | Multiple references |
person() | string | Reference to a DID |
person.many() | string[] | Multiple DID references |
checkbox() | boolean | Alias for boolean |
file() | object | File attachment |
formula() | computed | Computed at read time |
rollup() | computed | Aggregation over relations |
See Property Types for full details on each type.
A node is an instance of a schema. Every node has base fields that are managed automatically by the store — you never define or set these yourself:
interface NodeBase { id: string // Unique node ID (auto-generated) schemaIRI: string // Schema identifier createdAt: number // Set at creation time updatedAt: number // Updated on every change createdBy: string // Author DID updatedBy: string // Last modifier's DID}When you read a node via useQuery or useNode, you get a FlatNode — the base fields merged with the schema properties:
// FlatNode<TaskSchema>{ id: 'abc123', schemaIRI: 'xnet://app/task', createdAt: 1706000000, updatedAt: 1706000100, createdBy: 'did:key:z6Mk...', title: 'Ship the feature', priority: 1, done: false, status: 'doing', project: 'project-456'}NodeStore
Section titled “NodeStore”The NodeStore is the central data layer that manages CRUD operations and change tracking:
- Create —
store.create(schema, properties)→ new node with signed change - Read —
store.get(id)orstore.query(schema, filter)→ node(s) - Update —
store.update(id, changes)→ signed change with Lamport timestamp - Delete —
store.delete(id)→ soft delete with signed change
Every mutation creates a Change<T> record that is signed, hash-chained, and propagated to peers.
Change<T>
Section titled “Change<T>”The Change<T> type is the fundamental unit of sync:
interface Change<T> { id: string // Unique change ID type: string // e.g., 'create-task', 'update-task' payload: T // The actual data hash: ContentId // cid:blake3:... content-addressed parentHash: ContentId | null // Previous change in the chain authorDID: DID // Who made this change signature: Uint8Array // Ed25519 over the hash wallTime: number // Wall clock (display only) lamport: LamportTimestamp // Causal ordering}Changes form a hash chain — each change references its parent’s hash. This creates a verifiable audit trail where:
- Tampering is detectable — altering a change invalidates its hash and breaks the chain
- Authorship is provable — every change is signed with Ed25519
- Ordering is deterministic — Lamport timestamps provide causal ordering across peers
Transactions
Section titled “Transactions”For atomic multi-node operations, use transactions:
const result = mutate.transaction((tx) => { const projectId = tx.create(ProjectSchema, { name: 'Acme' }) tx.create(TaskSchema, { title: 'Setup', project: projectId }) tx.create(TaskSchema, { title: 'Launch', project: projectId })})Transactions are batched — all changes share a batchId and are applied atomically. Temporary IDs used within a transaction are resolved to real IDs when committed.
Further reading
Section titled “Further reading”- defineSchema — Schema definition API
- Property Types — All 16 property types
- Type Inference — How TypeScript types flow from schemas to hooks
- Relations — Linking nodes together