Dev Investigation Notes

A WebSocket room host loses control after leaving and rejoining — restoring it with a localStorage token and a server-side hash

This is a translated article. Read the original in Korean ↗

We were moving formation, a web app where one person (the leader) plays a video in a dance rehearsal studio while everyone else follows the same point in the video and formation on their own phones, to a WebSocket room architecture on Cloudflare Workers. Creating rooms, joining them, and attaching choreography data all worked locally, but once the leader left a room, there was no way to return as the leader . The room stayed alive with nobody able to control it. This article uses plain WebSockets on Cloudflare Durable Objects, but the principle of "keep the token in the browser, store only its hash on the server, and compare on reconnect" applies just as well to other real-time servers such as socket.io.

Conclusion

Define the host as "the person holding the secret token," not "the connection that created the room." Then build a UI entry point for returning with that token.

  1. When creating a room, the client generates a random token, stores it in localStorage, and sends only its hash to the server
  2. The leader page sends the token when connecting or reconnecting. The server compares hashes: a match grants the leader role; a missing or incorrect token downgrades the connection to a viewer
  3. Never include the hash in room listings or responses
  4. In the lobby, rooms with a token in localStorage (= rooms I created) get a "리더로 재입장" (Rejoin as leader) button. Other rooms get a "참여" (Join) button
  5. The server distinguishes "disconnection" (keep the room) from "explicit closure" (delete the room). If disconnecting means closing, there is no room to return to

The following is a generic implementation example, since no code remains in the session record. The session confirms only that hashing uses crypto.subtle.digest . The key name, token length, and where the token is passed are illustrative.

// Client — when creating a room (generic implementation example)
const token = [...crypto.getRandomValues(new Uint8Array(32))]
  .map(b => b.toString(16).padStart(2, '0')).join('');
async function sha256Hex(s) {
  const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s));
  return [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('');
}
localStorage.setItem(`leaderToken:${roomId}`, token);      // Original stays in the browser only
await createRoom({ name, leaderTokenHash: await sha256Hex(token) }); // Only the hash goes to the server

// Server (room object) — on connection (generic implementation example)
const presented = url.searchParams.get('token');
const role = presented && (await sha256Hex(presented)) === this.leaderTokenHash
  ? 'leader' : 'viewer';                                    // On mismatch, downgrade instead of disconnecting

Situation

Symptom

Cause

Two things were missing at the same time.

  1. There was no way to prove authority. The leader role belonged to the connection that originally created the room, and a new connection had no way to prove "I'm that leader." Adding tokens also put "prevent just anyone from becoming the leader" on the fix list
  2. There was no entry point for returning. The server-side URL existed, but the lobby had no way to recognize "a room I created," so there was no button either. The token left in the browser is the lobby's only clue

The initial design table included this feature as "reconnect within 1h with leaderToken → regain leader privileges" . But after the server relay was built, "leaderToken reconnection" was postponed to "Phase 4" , and the next summary described it as "(optional — for now, continue by reselecting the video on reconnect)" . While a feature from the design was deferred twice, the person testing it found the gap first.

Fix

One commit changed five places .

LocationChange
Transport wrapper (live.js)Added token generation and hashing utilities
Leader pageGenerate and store a token when creating a room, present it on connection, and handle role rejection (downgrade to viewer)
Lobby objectAccept and store the token hash, but omit it from public listings
Room objectValidate the leader token, downgrade to viewer on mismatch, and never send the hash to clients
LobbyRooms with a token get "리더로 재입장" (Rejoin as leader) (→ leader.html); rooms without one get "참여" (Join)

Verification:

Even after rejoining, you have to select the video file again. The file lives on each person's device, not on the server .

Bonus — token hashing only works over https (or localhost)

crypto.subtle exists only in secure contexts (https or localhost). The conclusion was that accessing a LAN IP such as http://<PC IP>:8787 from a phone causes token hashing (and video fingerprinting) to fail, preventing room creation, so phone testing would use the https deployment URL . After deployment, secure context: true, crypto.subtle: true was confirmed .

An apparent limitation of this design (not discussed in the session): since the token exists only in one browser's localStorage, you cannot return as the leader from another device, another browser, an incognito window, or after clearing storage.

Reporter's Notes — the URL existed, but there was no door

Park Doeun
That web app where the leader plays a video in the rehearsal studio and everyone follows along on their phones — creating rooms and attaching choreography data all worked. But when I closed the leader window and tried to return, there was no way to get back in as the leader
Jung Bada
Don't you just need the URL? leader.html?room=코드 has always been there. Go straight there and you're the leader
Park Doeun
I know the address exists, but every room in the lobby just says "참여" (Join). Joining always sends you to the viewer screen
Jung Haneul
And if typing the address makes you the leader, like Bada says, that's an even bigger problem. It means any viewer could enter that address and pause the video whenever they want
Jung Bada
Hmm… then let's remember the IP of whoever created the room and give them the leader role back if it's the same IP
Park Dohyun
A phone's IP changes just by switching from Wi-Fi to LTE. And everyone on the same studio router might appear to have the same IP. Identifying people by their network is costly and easy to get wrong
Jung Bada
Fair. Then we can give them something like a password when they create the room
Jung Haneul
Right, we already wrote that into the original design. Have the browser generate and store a random token when creating the room, then present it when reconnecting
Park Doeun
If it was written down, why has it been missing all this time?
Jung Haneul
After building the relay server, we pushed it to "the next phase," and then it got demoted to "optional"
Park Dohyun
That's why you found it first. Defer a planned feature as optional, and the person using it will trip over the gap first
Park Doeun
Got it. Then we can store the token as-is on the server too, right? Compare the two when someone joins
Jung Haneul
Keep the original only in the browser and only the hash on the server. A hash is like a fingerprint that can't be reversed to recover the original value. Even if the server-side value leaks, it can't be used to impersonate the leader
Jung Bada
Leak where? The room list only shows names and headcounts
Jung Haneul
The lobby that builds that list also holds the hash. That's why we explicitly made sure it gets stored but excluded from the public list
Jung Bada
Then if someone connects with the wrong token, we can just close the socket immediately. Nice and clean
Park Dohyun
Disconnect them and people without tokens can't even watch. Downgrading them to viewers when the token is wrong, so they can only watch, costs everyone in the room less
Park Doeun
How do we decide which lobby button to show? The server doesn't tell me who created the room
Jung Bada
Your browser knows. If that room's token is in localStorage — the little storage space that persists in your browser — it's a room you created. Just show "리더로 재입장" (Rejoin as leader) for those rooms
Park Doeun
I tried it. The right token gets the leader role, an incorrect or missing token gets downgraded to viewer, and the hash doesn't show up anywhere in the listings or responses
Park Dohyun
So you've confirmed leaving on a phone and reconnecting as the leader too?
Park Doeun
I haven't checked that far yet. I've only confirmed that the leader gets role leader with a token at the deployment URL
Jung Bada
Just open the PC's IP from your phone. No need to wait for deployment
Jung Haneul
That won't work. crypto.subtle, which we use for hashing, only exists on https or localhost, so accessing an IP over http blocks you right at room creation
Park Doeun
Oh, that's why we only tested phones against the https deployment URL. I'll put checking the rejoin button on the list for the next rehearsal studio session
Park Dohyun
So the host is the person holding the secret, not the connection that created the room. And even if the server provides a route, without a door on the screen, the feature might as well not exist