WebSocket remote functions for SvelteKit

Author realtime streams and mutations the way you write remote functions: validated, type-safe, multiplexed over a single socket, served by Bun or Node.

bun add svelte-ws SvelteKit 3 · Svelte 5 · Bun 1.4+ or Node 22.17+

// chat.socket.ts — server code, compiled away from the client
import { stream, mutate } from 'svelte-ws';
import { publish, subscribe } from 'svelte-ws/pubsub';
import * as v from 'valibot';

export const messages = stream(v.object({ room: v.string() }), async function* ({ room }) {
	yield* subscribe(`room:${room}`);
});

export const send = mutate(v.object({ room: v.string(), text: v.string() }), ({ room, text }) =>
	publish(`room:${room}`, { text })
);
<script>
	// the same import, in a component — now it's a reactive WS client
	import { messages, send } from './chat.socket';

	const feed = messages({ room: 'general' });
</script>

{feed.current?.text}
<button onclick={() => send({ room: 'general', text: 'hi' })}>Send</button>

Wire it up once

A plugin and an adapter, with nothing added to hooks.server.ts and no second server: the dev server serves the endpoint itself.

// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import adapter from 'svelte-ws/adapter-node'; // or 'svelte-ws/adapter-bun'

export default defineConfig({
	plugins: [sveltekit({ adapter: adapter() })]
});

Own the server

Optional socket.config.ts: startup code, authentication at the upgrade, per-connection limits and error handling, identical in dev and production.

// socket.config.ts — one config for `vite dev` and the built server
import { defineSocketServer } from 'svelte-ws/config';
import { setPubSub } from 'svelte-ws/pubsub';

export default defineSocketServer({
	// startup: connect, swap the bus, return a cleanup
	init: async () => {
		setPubSub(createRedisPubSub());
		return () => db.close();
	},

	// authenticate once, per connection
	upgrade: ({ request }) => {
		const session = read(request.headers.get('cookie'));
		if (!session) return new Response(null, { status: 401 });
		return { data: { userId: session.userId } };
	},

	limits: { maxOperations: 64 },
	handleError: (error) => logger.error(error)
});