Skip to main content
This guide shows you how to use CollabKit’s Stores module to build a shared task board where all changes are synced in real time across connected clients.

What You’ll Build

  • A shared task list with real-time sync
  • Type-safe store operations with schema validation
  • Live update rendering when other users make changes

Prerequisites

  • A CollabKit server running with a room and users created
  • @collab-kit/client and @collab-kit/utils installed

Step 1: Define Your Schema

Create a schema using defineStores(). This gives you full type safety and validation:
import { defineStores } from '@collab-kit/utils';

const stores = defineStores({
  tasks: {
    title: { type: 'string', required: true },
    completed: { type: 'boolean', default: false },
    assignee: { type: 'string' },
    priority: { type: 'number', default: 0 },
  },
});

Schema Rules

  • required: true — field must be present when calling set() (unless it has a default)
  • default: value — applied automatically when the field is missing on set()
  • Fields without required: true become optional in TypeScript types

Step 2: Initialize the Client with Stores

import CollabKitClient from '@collab-kit/client';

const client = new CollabKitClient({
  serverUrl: 'https://api.collab-kit.com',
  authToken: '<jwt-token>',
  stores,
});

await client.connect();
await client.join();

Step 3: Create, Read, Update, Delete

// CREATE -- 'completed' defaults to false, 'priority' defaults to 0
const task = await client.stores.tasks.set({
  key: 'task-1',
  value: { title: 'Design mockups', assignee: 'user-001' },
});

// READ one
const fetched = await client.stores.tasks.get({ key: 'task-1' });
// { title: 'Design mockups', completed: false, assignee: 'user-001', priority: 0 }

// READ all
const allTasks = await client.stores.tasks.getAll();
// [{ key: 'task-1', value: { ... } }, ...]

// UPDATE (partial -- only changes specified fields)
await client.stores.tasks.update({
  key: 'task-1',
  value: { completed: true },
});

// DELETE
await client.stores.tasks.delete({ key: 'task-1' });

Step 4: Listen for Real-Time Changes

Store events fire when other clients make changes:
// Listen for any change in the tasks store
client.stores.tasks.on('changed', ({ key, action, value }) => {
  console.log(`[${action}] ${key}:`, value);
  // action is 'set', 'update', or 'delete'
  renderTaskList();
});

// Listen for changes to a specific key
client.stores.tasks.on('task-1', (value) => {
  console.log('task-1 changed:', value);
  updateTaskUI('task-1', value);
});

Step 5: Build the Task Board UI

const listEl = document.getElementById('task-list');
const formEl = document.getElementById('task-form');
const inputEl = document.getElementById('task-input') as HTMLInputElement;

// Render all tasks
async function renderTaskList() {
  const tasks = await client.stores.tasks.getAll();

  listEl.innerHTML = tasks.map(({ key, value }) => `
    <div class="task ${value.completed ? 'completed' : ''}" data-key="${key}">
      <input type="checkbox" ${value.completed ? 'checked' : ''}
        onchange="toggleTask('${key}', this.checked)" />
      <span>${value.title}</span>
      ${value.assignee ? `<small>Assigned to: ${value.assignee}</small>` : ''}
      <button onclick="deleteTask('${key}')">Delete</button>
    </div>
  `).join('');
}

// Add a new task
formEl.addEventListener('submit', async (e) => {
  e.preventDefault();
  const title = inputEl.value.trim();
  if (!title) return;

  const key = `task-${Date.now()}`;
  await client.stores.tasks.set({
    key,
    value: { title, assignee: client.userId },
  });

  inputEl.value = '';
  renderTaskList();
});

// Toggle completion
window.toggleTask = async (key: string, completed: boolean) => {
  await client.stores.tasks.update({ key, value: { completed } });
  renderTaskList();
};

// Delete a task
window.deleteTask = async (key: string) => {
  await client.stores.tasks.delete({ key });
  renderTaskList();
};

// Re-render when other clients make changes
client.stores.tasks.on('changed', () => renderTaskList());

// Initial render
renderTaskList();

Multiple Stores

You can define multiple stores for different data types:
const stores = defineStores({
  tasks: {
    title: { type: 'string', required: true },
    completed: { type: 'boolean', default: false },
  },
  settings: {
    theme: { type: 'string', default: 'light' },
    sortBy: { type: 'string', default: 'created' },
  },
  labels: {
    name: { type: 'string', required: true },
    color: { type: 'string', required: true },
  },
});

// Each store is accessed independently
await client.stores.tasks.set({ key: 'task-1', value: { title: 'Ship it' } });
await client.stores.settings.set({ key: 'user-prefs', value: { theme: 'dark' } });
await client.stores.labels.set({ key: 'label-1', value: { name: 'Bug', color: '#ef4444' } });

Syncing on Reconnect

When the client reconnects after a network interruption, call sync() to reload the latest state:
client.socket.on('reconnected', async () => {
  await client.stores.tasks.sync();
  renderTaskList();
});

Next Steps

  • Add comments to individual tasks
  • Use presence to show which task a user is viewing
  • Set up webhooks to notify external services when tasks change