Skip to content

Quick Start

  • Node.js 22+
  • pnpm (or npm/yarn)
  • Basic React knowledge

A task manager that works offline, syncs peer-to-peer, and persists to the browser’s IndexedDB. No server required.

  1. Create a React project and install xNet

    Terminal window
    pnpm create vite my-app --template react-ts
    cd my-app
    pnpm add @xnet/react @xnet/data
  2. 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 const on the options array gives you literal type inference — status is typed as 'todo' | 'doing' | 'done', not just string.

  3. Wrap your app in XNetProvider

    The 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 securely
    const authorDID = 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK'
    const signingKey = new Uint8Array(32) // Replace with real Ed25519 key
    export function App() {
    return (
    <XNetProvider config={{ authorDID, signingKey }}>
    <TaskList />
    </XNetProvider>
    )
    }
  4. Read data with useQuery

    src/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}>
    <input
    type="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. useQuery returns reactive data that auto-updates when you (or a peer) mutate. useMutate gives you type-safe create, update, and remove methods.

  5. Run it

    Terminal window
    pnpm dev

    Open http://localhost:5173. Create some tasks. Refresh the page — they persist. Open a second tab — they sync.

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 safetytask.status is 'todo' | 'doing' | 'done', not string
  • Schema validation — invalid data is rejected at creation time
  • Offline-first — works without any network connection
  • Cryptographic signing — every change is signed with Ed25519
  • Directorysrc/ - schema.ts defineSchema() definitions - App.tsx XNetProvider - TaskList.tsx useQuery + useMutate

Use useNode to add a collaborative rich text editor to each task:

const { doc } = useNode(TaskSchema, taskId)
// Pass doc to TipTap's Collaboration extension