Full example: real-time todo list

Here’s a complete example tying everything together - adapter config, Vite plugin, server actions with platform.topic(), and the reactive crud store on the client.

Open the page in two browser tabs. Create, toggle, or delete a todo in one tab → it appears in the other tab instantly.


Adapter config

svelte.config.js

import adapter from 'svelte-adapter-uws';

export default {
  kit: {
    adapter: adapter({
      websocket: true
    })
  }
};

vite.config.js

import { sveltekit } from '@sveltejs/kit/vite';
import uws from 'svelte-adapter-uws/vite';

export default {
  plugins: [sveltekit(), uws()]
};

The Vite plugin gives you a working WebSocket server during npm run dev. Without it, WebSocket only works in production (node build).


Server actions

src/routes/todos/+page.server.js

import { db } from '$lib/server/db.js';

export async function load() {
  return { todos: await db.getTodos() };
}

export const actions = {
  create: async ({ request, platform }) => {
    const text = (await request.formData()).get('text');
    const todo = await db.createTodo(text);
    platform.topic('todos').created(todo);
  },

  toggle: async ({ request, platform }) => {
    const id = (await request.formData()).get('id');
    const todo = await db.toggleTodo(id);
    platform.topic('todos').updated(todo);
  },

  delete: async ({ request, platform }) => {
    const id = (await request.formData()).get('id');
    await db.deleteTodo(id);
    platform.topic('todos').deleted({ id });
  }
};

Each action mutates the database, then broadcasts the change to every connected client via platform.topic(). The created, updated, and deleted helpers match the events that the crud store listens for.


Client page

src/routes/todos/+page.svelte

<script>
  import { crud, status } from 'svelte-adapter-uws/client';

  let { data } = $props();
  const todos = crud('todos', data.todos);
</script>

{#if $status === 'open'}
  <span>Live</span>
{/if}

<form method="POST" action="?/create">
  <input name="text" placeholder="New todo..." />
  <button>Add</button>
</form>

<ul>
  {#each $todos as todo (todo.id)}
    <li>
      <form method="POST" action="?/toggle">
        <input type="hidden" name="id" value={todo.id} />
        <button>{todo.done ? 'Undo' : 'Done'}</button>
      </form>
      <span class:done={todo.done}>{todo.text}</span>
      <form method="POST" action="?/delete">
        <input type="hidden" name="id" value={todo.id} />
        <button>Delete</button>
      </form>
    </li>
  {/each}
</ul>

The crud store takes the topic name and the initial data from load(). It automatically handles created, updated, and deleted events - inserting, patching, or removing items by id.


How it works

  1. The page loads with server-rendered todos from load().
  2. The crud store subscribes to the todos topic over WebSocket.
  3. When a user submits a form, SvelteKit runs the server action.
  4. The action writes to the database, then calls platform.topic('todos').created(todo) (or .updated(), .deleted()).
  5. The adapter broadcasts that event to every WebSocket client subscribed to todos.
  6. Each client’s crud store receives the event and updates its local array reactively.

No polling, no manual refetching - just forms and stores.

Was this page helpful?