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, checkbox, select, relation } from '@xnetjs/data'
const TaskSchema = defineSchema({ name: 'Task', namespace: 'xnet://my-app/', properties: { title: text({ required: true }), priority: number({ default: 0 }), done: checkbox({ default: false }), status: select({ options: [{ id: 'todo', name: 'To Do' }] as const }), project: relation({ target: 'xnet://my-app/Project@1.0.0' }) }})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 15 property types:
| Type | Value | Notes |
|---|---|---|
text() | string | Plain text |
number() | number | Numeric value |
checkbox() | 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({ multiple: true }) | string[] | Multiple references |
person() | string | Reference to a DID |
person({ multiple: true }) | string[] | Multiple DID references |
file() | object | File attachment |
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) schemaId: 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', schemaId: 'xnet://app/task@1.0.0', 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
Batched mutations
Section titled “Batched mutations”For multi-node operations, use useMutate().mutate(...):
await mutate([ { type: 'create', schema: ProjectSchema, id: 'project-acme', data: { name: 'Acme' } }, { type: 'create', schema: TaskSchema, data: { title: 'Setup', project: 'project-acme' } }, { type: 'create', schema: TaskSchema, data: { title: 'Launch', project: 'project-acme' } }])The current mutate() API returns per-operation results and runs through the active data bridge.
Further reading
Section titled “Further reading”- defineSchema — Schema definition API
- Property Types — All 15 property types
- Type Inference — How TypeScript types flow from schemas to hooks
- Relations — Linking nodes together