# 4. Give the game a socket route

HTTP answers a request. A WebSocket stays open so either side can speak. The URL selects the service; the message type selects a handler.

For this game the URL is `/match?redwebVersion=1`. A request looks like:

```json
{ "v": "1", "type": "move", "payload": { "cell": 4, "revision": 0 } }
```

There is no `message.action` dispatcher. There are separate `Join`, `Move`, and `Resume` handlers.

<!-- source: src/contract.ts -->

The contract is shared by server and client. Zod validates payloads and gives TypeScript their types. An invalid cell is a malformed command; a valid cell played out of turn is a game-rule rejection. The former is a protocol error; the latter returns a recoverable `notice` without pretending the move succeeded.

<!-- source: src/routes.ts -->

`matchRoute()` creates an isolated room collection and captures the application's identity lookup. The returned `MatchRoute` is the ordinary route class registered with `defineApp`. This small factory keeps independent test applications from accidentally sharing matches.

Every command obtains the account from the authenticated socket context and current session. The payload cannot nominate another user. Publishing sends each member their own `you` mark and rechecks sessions before sending. The close callback updates presence. A second tab for one account does not become a third player or make that account appear offline when only one of its tabs closes.

**Checkpoint:** `npm test` connects real clients to this route and attempts both valid and hostile traffic. `orderedMessages` orders one socket's commands; the synchronous rules still enforce the shared game's turn and revision across sockets.
