Tutorial 02
Write a socket contract beside a route
Put the realtime server file beside the SvelteKit route that renders the page.
Outcome
You will create src/routes/rooms/[roomId]/chat.socket.ts, validate messages from the page, and broadcast accepted messages to the current room.
Route file
Create chat.socket.ts beside the page
This file runs on the server. The page can still import its exported chat value because the Vite plugin turns that import into a browser client.
One route owns one realtime contract
The folder src/routes/rooms/[roomId] already owns the roomId URL value. Put chat.socket.ts in that folder so validation, room selection, and broadcasts stay next to the page that uses them.
Previously: The Start page added the Vite transform and Node adapter wrapper that make this file importable from the browser page.
Code focus: This first version defines the message payload, the auto-joined room, and the browser-to-server send action in one file.
Lives in
src/routes/rooms/[roomId]/chat.socket.ts
server contract for this chat page · Create this file next to the page that renders the room. It defines what the page may send and what connected pages may receive.
Route map
src/routes/rooms/[roomId]/
+page.svelte
chat.socket.ts <- realtime contract
src/hooks.socket.ts
import * as v from 'valibot';
import { event, room, socket } from 'liverpc/server';
const messageSchema = v.object({
// The browser may only send a text string that matches this schema.
text: v.string()
});
export const chat = socket({
toClient: {
message: v.object({
id: v.string(),
roomId: v.string(),
text: v.string(),
sentAt: v.string()
})
},
rooms: {
chat: room.private(({ params }) =>
// The [roomId] URL segment chooses the Socket.IO group for this route.
['chat', params.roomId],
{ autoJoin: true }
)
},
fromClient: {
send: event(messageSchema, ({ rooms, params, data }) => {
const message = {
id: crypto.randomUUID(),
roomId: params.roomId,
text: data.text,
sentAt: new Date().toISOString()
};
// Broadcast to every browser currently joined to this route room.
rooms.chat.emit.message(message);
// The page awaiting room.emit.send receives this typed result.
return { id: message.id };
})
}
});Result: The route now has a typed server file. The browser page can send messages, but it cannot import liverpc/server or choose a different room.
Note: The page imports ./chat.socket, not liverpc/server. LiveRPC replaces that import with browser code during the Vite build.
Checkpoint
Hover the page import in your editor. The emit payload and listener payload should be inferred from this server file.