CodX Editor has live collaboration sessions. Someone starts a room, other people join with a link, and the person who started it holds controls the others do not: kick, ban, lock the room, freeze editing.

Hosts lose connection. Phones sleep, trains go into tunnels, laptops close. So the server did what looked like the sensible thing: when the host's socket dropped and there was no co-host, it picked somebody still in the room and made them host until the real one came back.

It was reported as a bug by someone who watched a stranger walk into an empty room and be handed the entire session.

The sequence

The host closed their tab and left for good. Socket.IO had not yet noticed — a clean tab close is fast, but a phone losing signal takes a ping timeout, which can be the better part of a minute. In that window the server still had the host in the participant list.

A new person opened the invite link. The room did not look empty to the server, so they joined as a normal participant. Then the ping timeout expired, the host was removed, and the promotion code ran:

const nextHost = session.participants[
  Math.floor(Math.random() * session.participants.length)
];
nextHost.role = "host";

There was one participant. They had been in the room for forty seconds. They now had kick, ban and every permission control in it.

The assumption underneath

The promotion code was written for a host who dropped for a moment and was coming straight back. Standing in for them keeps the room usable, and the role is handed back on return. That is a reasonable feature.

The problem is that the server cannot tell the two cases apart. A host who closed the tab and a host whose train went into a tunnel produce the same event. There is no signal that distinguishes "back in ten seconds" from "gone".

So the code was quietly making a guess, and the guess was wrong in precisely the case where being wrong is most expensive.

What we do instead

The host seat is left empty. Nobody is promoted.

session.hostSocketId = null;

That is not a new state we had to invent. It is exactly what already happened when a co-host was present, so every part of the system that deals with a room already handled it.

The important part is what is not cleared: hostName and hostDeviceId keep pointing at the real host. The room still knows who it belongs to. Our join code already refused to let a stranger into an empty room until the original host returned, and that guard reads those exact fields, so it keeps holding the room. When the host comes back they are recognised by device id or name and take the seat straight back.

The cost, stated honestly

If the host drops and there is no co-host, nobody in that room can moderate until they return. People can still write code together and still chat. They just cannot kick, ban or change permissions.

That is a real loss and it is worth naming rather than pretending the fix was free. But the two outcomes are not symmetrical. A room without a moderator for five minutes is an inconvenience. A stranger with ban powers over a classroom is a different kind of problem, and it happens without anybody deciding it should.

The rule we took from it

Authority should be handed over, not fallen into. If a system grants power because of a timeout rather than because somebody chose to grant it, the grant will eventually land on the wrong person, and it will do so silently.

Co-hosts still work exactly as before, because a co-host is a decision the host actually made. That is the whole difference.