Tutorial 04

Choose who can join each room

Continue the chat route by deciding which server value chooses each Socket.IO group.

Outcome

You will know when to use the [roomId] URL param, when to use values from src/hooks.socket.ts, and when the page may request a validated join.

1

Private rooms

Use the route param for the visible chat room

Use a private room when server code already knows the correct Socket.IO group. For this chat route, [roomId] in the URL is enough to choose the group.

Private room means the page does not choose the group

In src/routes/rooms/[roomId]/chat.socket.ts, rooms.chat uses params.roomId. A browser on /rooms/general joins the general room because the route param is general, not because the page sent a separate join request.

Terms

Previously: You already created chat.socket.ts with toClient.message and fromClient.send. This step focuses on the rooms block inside that same file.

Code focus: This is not a standalone snippet. It is the rooms section inside export const chat = socket({ ... }) from the contract you already wrote.

Lives in

src/routes/rooms/[roomId]/chat.socket.ts

edit the rooms block inside the route contract The rooms block belongs in the same server file that reads params.roomId and broadcasts messages. The page only calls the generated browser client.

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 · rooms block inside chat contract
import { room, socket } from 'liverpc/server';

export const chat = socket({
  // Keep toClient.message and fromClient.send from the previous page.

  rooms: {
    chat: room.private(({ params }) =>
      // params.roomId is the [roomId] part of the current URL.
      ['chat', params.roomId],
      {
        // Every connected page instance joins this route room immediately.
        autoJoin: true
      }
    )
  }
});

Result: The active chat room is chosen by chat.socket.ts from the URL param. The page can listen and send, but it never sends a private room identifier.

Checkpoint

Open /rooms/general and /rooms/random in separate tabs. A message in general should not appear in random because the two URLs produce different room identifiers.

2

Session data

Use socket locals for tenant or account rooms

Use route params for the current page. Use socket locals for identity or account data that must come from cookies, headers, or your session lookup.

Socket locals are returned by src/hooks.socket.ts

When the socket connects, src/hooks.socket.ts reads the request and returns locals. A room definition can use locals.user.tenantId without letting the browser send tenantId.

Terms

Previously: The previous step used params.roomId for messages tied to one URL. This step uses locals.user.tenantId for messages tied to the signed-in user account.

Code focus: This excerpt adds only the tenant room. Keep it inside the same rooms object as rooms.chat in chat.socket.ts.

Lives in

src/routes/rooms/[roomId]/chat.socket.ts and src/hooks.socket.ts

room definition plus connection setup chat.socket.ts defines the tenant room. src/hooks.socket.ts supplies locals.user before any room code reads locals.user.tenantId.

Route map

src/hooks.socket.ts <- returns locals.user

src/routes/rooms/[roomId]/

chat.socket.ts <- reads locals.user.tenantId

typescript src/routes/rooms/[roomId]/chat.socket.ts · tenant room variant
rooms: {
  chat: room.private(({ params }) =>
    ['chat', params.roomId],
    { autoJoin: true }
  ),

  tenant: room.private(({ locals }) =>
    // src/hooks.socket.ts returned locals.user for this connection.
    ['tenant', locals.user.tenantId]
  )
}

Result: The same contract can broadcast either to the current URL room or to the signed-in tenant without accepting tenantId from browser code.

Checkpoint

Connect without a valid session and confirm the tenant room fails on the server instead of accepting a tenantId from the page.

3

Exposed rooms

Let the browser request a join, then authorize it

Use an exposed room when the user makes a real choice in the page, such as selecting a project tab. The page sends the choice, then the server decides whether to join.

Exposed does not mean trusted

The browser sends a request such as "join project A." The server validates the payload, checks locals.user, and either joins the project room or rejects the request.

Terms

Previously: Private rooms handled rooms chosen by the URL and rooms chosen by the logged-in user. Use an exposed room only when the browser has a real choice to make.

Code focus: Read the server join handler first, then the page call. The client call is typed by the schema in the contract.

Lives in

src/routes/rooms/[roomId]/chat.socket.ts and src/routes/rooms/[roomId]/+page.svelte

server join handler plus page call The authorization check lives in chat.socket.ts. The page only calls the generated join method with a typed payload.

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 · exposed project room
rooms: {
  project: room.expose(
    v.object({ projectId: v.string() }),
    ({ data, locals }) => {
      // Treat the browser payload as a request, not proof of access.
      if (!locals.user.projectIds.includes(data.projectId)) {
        return false;
      }

      return ['project', data.projectId];
    }
  )
}

Lives in

src/routes/rooms/[roomId]/chat.socket.ts and src/routes/rooms/[roomId]/+page.svelte

server join handler plus page call The authorization check lives in chat.socket.ts. The page only calls the generated join method with a typed payload.

Route map

src/routes/rooms/[roomId]/

+page.svelte

chat.socket.ts <- realtime contract

src/hooks.socket.ts

svelte src/routes/rooms/[roomId]/+page.svelte · typed join call
<script lang="ts">
  import { chat } from './chat.socket';

  const room = chat();

  async function openProject(projectId: string) {
    // Typed from the room.expose schema in chat.socket.ts.
    await room.join.project({ projectId });
  }
</script>

Result: The page can request a project subscription, but only chat.socket.ts decides whether that request joins a Socket.IO room.

Note: The client call is typed, but authorization still belongs on the server.

Checkpoint

Try joining a project the user cannot access. The channel should reject the join without leaking room state.

Run the safe realtime lab