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.
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.
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
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.
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.
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
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.
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.
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
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
<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.