Quick Start
Prerequisites
Section titled “Prerequisites”- Node.js 22+
- pnpm (or npm/yarn)
- Basic React knowledge
What you’ll build
Section titled “What you’ll build”A task manager that works offline, syncs peer-to-peer, and persists to the browser’s IndexedDB. No server required.
-
Create a React project and install xNet
Terminal window pnpm create vite my-app --template react-tscd my-apppnpm add @xnet/react @xnet/data -
Define your schema
Create a file that describes the shape of your data:
src/schema.ts import { defineSchema, text, select, checkbox } from '@xnet/data'export const TaskSchema = defineSchema({name: 'Task',namespace: 'xnet://my-app/',properties: {title: text({ required: true }),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 })}})The
as conston the options array gives you literal type inference —statusis typed as'todo' | 'doing' | 'done', not juststring. -
Wrap your app in
XNetProviderThe provider initializes the NodeStore, storage, and sync system:
src/App.tsx import { XNetProvider } from '@xnet/react'import { TaskList } from './TaskList'// In production, generate and persist these securelyconst authorDID = 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK'const signingKey = new Uint8Array(32) // Replace with real Ed25519 keyexport function App() {return (<XNetProvider config={{ authorDID, signingKey }}><TaskList /></XNetProvider>)} -
Read data with
useQuerysrc/TaskList.tsx import { useQuery, useMutate } from '@xnet/react'import { TaskSchema } from './schema'export function TaskList() {const { data: tasks } = useQuery(TaskSchema, {orderBy: { createdAt: 'desc' }})const { create, update, remove } = useMutate()const addTask = () => {create(TaskSchema, { title: 'New task', status: 'todo' })}return (<div><button onClick={addTask}>Add Task</button><ul>{tasks.map((task) => (<li key={task.id}><inputtype="checkbox"checked={task.completed}onChange={() =>update(TaskSchema, task.id, {completed: !task.completed,status: task.completed ? 'todo' : 'done'})}/><span>{task.title}</span><button onClick={() => remove(task.id)}>Delete</button></li>))}</ul></div>)}That’s it.
useQueryreturns reactive data that auto-updates when you (or a peer) mutate.useMutategives you type-safe create, update, and remove methods. -
Run it
Terminal window pnpm devOpen
http://localhost:5173. Create some tasks. Refresh the page — they persist. Open a second tab — they sync.
What just happened
Section titled “What just happened”Without writing a single line of backend code, you got:
- Persistent storage — data lives in IndexedDB, survives refreshes
- Reactive queries — UI updates automatically when data changes
- Type safety —
task.statusis'todo' | 'doing' | 'done', notstring - Schema validation — invalid data is rejected at creation time
- Offline-first — works without any network connection
- Cryptographic signing — every change is signed with Ed25519
Project structure
Section titled “Project structure”Directorysrc/ - schema.ts defineSchema() definitions - App.tsx XNetProvider - TaskList.tsx useQuery + useMutate
- …
Next steps
Section titled “Next steps”Use useNode to add a collaborative rich text editor to each task:
const { doc } = useNode(TaskSchema, taskId)// Pass doc to TipTap's Collaboration extensionPass signaling servers to XNetProvider to sync across devices:
<XNetProvider config={{ authorDID, signingKey, signalingServers: ['wss://your-hub.example.com'],}}>- Core Concepts — understand the mental model
- useQuery reference — filtering, sorting, pagination
- defineSchema reference — all schema options
- Property Types — all 16 property types