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.

1

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.

Terms

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

typescript src/routes/rooms/[roomId]/chat.socket.ts · first contract
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.

Run the safe realtime lab