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)
});Why svelte-ws
Remote functions, over WebSockets
Write *.socket.ts like *.remote.ts: stream() is a live query, mutate() is a command. One multiplexed connection per app.
One bus for every transport
publish/subscribe works the same from a socket handler, a remote command, or +server.ts. In-memory by default; swap in Redis to fan out across instances.
Bun and Node adapters
SvelteKit’s official adapter-node and adapter-bun, with a WebSocket endpoint added. Static files, environment variables and shutdown stay theirs.
One config for dev and prod
socket.config.ts runs at startup in vite dev and on the built server: init code, upgrade auth, limits, WebSocket tuning.
Reconnects, and resumes
The client reconnects with backoff and re-subscribes every stream. Give a stream a cursor and it picks up after the last value the client saw.
Tests without a server
createTestClient runs your socket modules over an in-memory socket in Vitest, with the real connection, schemas and upgrade hook.