Skip to content

defineSchema

import { defineSchema, text, select, checkbox, relation } from '@xnet/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.

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 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