Skip to content

defineSchema

import { defineSchema, text, select, checkbox, relation } from '@xnetjs/data'
export const TaskSchema = defineSchema({
name: 'Task',
namespace: 'xnet://my-app/',
properties: {
title: text({ required: true, maxLength: 500 }),
status: select({
options: [
{ id: 'todo', name: 'To Do', color: '#6366f1' },
{ id: 'doing', name: 'Doing', color: '#f59e0b' },
{ id: 'done', name: 'Done', color: '#10b981' }
] as const
}),
completed: checkbox({ default: false }),
project: relation({ target: 'xnet://my-app/Project' })
}
})
function defineSchema<P extends Record<string, PropertyBuilder>>(
options: DefineSchemaOptions<P>
): DefinedSchema<P>
FieldTypeRequiredDescription
namestringYesSchema name (e.g., 'Task'). Used in the schema IRI.
namespace`xnet://${string}/`YesNamespace for grouping. Must end with /.
propertiesRecord<string, PropertyBuilder>YesProperty definitions using builder functions.
extendsDefinedSchemaNoParent schema to inherit properties from.
document'yjs' | 'automerge'NoCRDT document type. Required for useNode rich text.
authorizationAuthorizationBlockNoRoles, actions, and encryption policy. See Authorization.

The schema’s unique identifier is ${namespace}${name}:

defineSchema({ name: 'Task', namespace: 'xnet://my-app/' })
// Schema IRI: 'xnet://my-app/Task'

Each property also gets an IRI: ${schemaId}#${propertyName}:

xnet://my-app/Task#title
xnet://my-app/Task#status
Field/MethodTypeDescription
schemaSchemaThe JSON-LD compatible schema object.
validate(node)(unknown) => ValidationResultValidate a node against this schema.
create(props, options)(props, options) => NodeCreate a new node (used internally).
is(node)(node) => booleanType guard — check if a node matches this schema.
_schemaIdstringThe computed schema IRI.
_propertiesPProperty builders (for type inference).
interface ValidationResult {
valid: boolean
errors: ValidationError[] // { path, message, value? }
}

Validation checks:

  • Required system fields (id, schemaId, createdAt, createdBy)
  • schemaId matches the schema
  • createdBy starts with did:key:
  • Each property’s constraints (required, min/max, pattern, etc.)

Add an authorization block to define who can read, write, delete, and share nodes of this schema:

import { defineSchema, text, person, relation } from '@xnetjs/data'
import { allow, role } from '@xnetjs/data/auth'
export const TaskSchema = defineSchema({
name: 'Task',
namespace: 'xnet://my-app/',
properties: {
title: text({ required: true }),
assignee: person(),
project: relation({ target: 'xnet://my-app/Project' as const })
},
authorization: {
roles: {
owner: role.creator(),
assignee: role.property('assignee'),
admin: role.relation('project', 'admin')
},
actions: {
read: allow('owner', 'assignee', 'admin'),
write: allow('owner', 'admin'),
delete: allow('owner', 'admin'),
share: allow('owner', 'admin')
},
publicProps: ['title'] // these fields are readable without decryption
}
})

Nodes are encrypted with a per-node content key and only users in the recipient list can decrypt them. The hub stores encrypted envelopes and filters queries by recipient list without ever decrypting content.

See the Authorization Guide for the full model including presets, useCan, useGrants, delegation, and key recovery.

Add document: 'yjs' to enable collaborative editing via useNode:

export const PageSchema = defineSchema({
name: 'Page',
namespace: 'xnet://my-app/',
properties: {
title: text({ required: true }),
icon: text()
},
document: 'yjs'
})

When document: 'yjs' is set, useNode creates a Y.Doc for the node and manages P2P sync for its content.

Use extends to inherit properties from a parent schema:

const BaseSchema = defineSchema({
name: 'Base',
namespace: 'xnet://my-app/',
properties: {
title: text({ required: true }),
tags: multiSelect({ options: ['draft', 'published'] as const })
}
})
const ArticleSchema = defineSchema({
name: 'Article',
namespace: 'xnet://my-app/',
extends: BaseSchema,
properties: {
body: text(),
publishedAt: date()
}
})
// ArticleSchema has: title, tags, body, publishedAt

When you create or update a node, property values are coerced through the property builder:

  • text() — calls String(value), trims whitespace
  • number() — calls Number(value), applies Math.round() if integer
  • select() — validates against option IDs, tries case-insensitive name matching as fallback
  • date() — accepts number, Date, or ISO string; normalizes to timestamp
  • email() — trims and lowercases
  • url() — auto-prepends https:// if no protocol

This means your data is always normalized regardless of what the user types.

In development, defineSchema warns if:

  • A text() property has a name that looks like a reference (e.g., targetId, parentId), suggesting you use relation() instead