Skip to content

Relations

Relations are how you connect nodes together in xNet. Instead of embedding nested objects, you store references between nodes using two specialized property types: relation() and person().

The relation() property stores a reference to another node by its ID. You can think of it like a foreign key in a relational database, except it works across peers in a local-first system. When you need to link a task to a project, or a comment to a post, relation() is the right choice.

import { defineSchema, relation, text } from '@xnetjs/data'
const TaskSchema = defineSchema({
name: 'Task',
namespace: 'xnet://my-app/',
properties: {
title: text(),
project: relation({ target: 'xnet://my-app/Project@1.0.0' }), // single reference
blockedBy: relation({ multiple: true }) // multiple references
}
})

Relations support both single and multiple cardinality. Use relation() for a single link and relation({ multiple: true }) for an array of links.

The person() property links to a user by their DID (Decentralized Identifier) rather than a node ID. This is useful for ownership, assignment, and authorship fields where you need to reference an identity rather than a piece of data.

const TaskSchema = defineSchema({
name: 'Task',
namespace: 'xnet://my-app/',
properties: {
title: text(),
assignee: person(), // single DID reference
watchers: person({ multiple: true }) // multiple DID references
}
})

When creating multiple related nodes at once, use explicit IDs so later operations can reference earlier creates in the same mutate call:

const { mutate } = useMutate()
await mutate([
{
type: 'create',
schema: ProjectSchema,
id: 'project-acme',
data: { name: 'Acme' }
},
{
type: 'create',
schema: TaskSchema,
data: { title: 'Setup', project: 'project-acme' }
}
])

Because the project node is created first (with a known ID), the task can safely reference it in the next operation.