Skip to content

Building a Universal WebSocket Server for 7u.pl

Note: The Polish version is the original. This translation is AI-generated and may contain minor errors.

WebSocket Server

As we add more interactive micro-tools to the 7u.pl ecosystem, the need arose to synchronize state in real-time between completely separated devices.

Example: our virtual-keyboard project must send events ("Q button pressed") from a smartphone, to a browser on a Mac, directly to a physical Raspberry Pi, bypassing local network restrictions on both sides.

Instead of building WebSocket logic separately for each application (which would quickly lead to infrastructure duplication), we decided to create a single, independent, very "dumb" communication server that does only one thing, but does it flawlessly: it listens to rooms and broadcasts messages between clients.

The Architecture of a "Dumb" Server

The key to universality is the lack of business logic on the server side. The server doesn't know what a "keyboard" is, what a "mouse" is, or what the structure of the JSON sent over the network looks like.

Technology Choice

We decided on Node.js combined with Socket.IO. Although native WebSockets (ws) are lighter, Socket.IO has a built-in excellent mechanism for organizing connections into so-called Rooms, which perfectly matches our needs for isolating different applications. Additionally, it relieves us of the problem of automatic reconnections on mobile devices with an unstable connection.

Source Code in a Nutshell

The core of the entire server is contained in just a few listeners on the io instance:

javascript
io.on('connection', (socket) => {
  // Join the client to a virtual room
  socket.on('join-room', (roomName) => {
    socket.join(roomName);
  });

  // Broadcast a message to others in the room (ignoring the sender)
  socket.on('broadcast', ({ roomName, eventName, payload }) => {
    socket.to(roomName).emit(eventName, payload);
  });
  
  // Broadcast a message to everyone in the room (including the sender)
  socket.on('broadcast-all', ({ roomName, eventName, payload }) => {
    io.to(roomName).emit(eventName, payload);
  });
});

That's literally it. The server accepts any object (payload), finds a room (roomName), wraps it in an event specified by the developer (eventName), and tosses it to the rest of the clients.

Security through CORS

Although the server knows nothing about the transmitted data itself, we wanted to ensure that our public endpoint would not turn into a free proxy server for half the internet.

The implementation required a strict rule to validate the Origin header to verify the connection at the earliest stage of the so-called handshake:

javascript
const io = new Server(httpServer, {
  cors: {
    origin: (origin, callback) => {
      // Allow absence of Origin (IoT devices/scripts)
      if (!origin) return callback(null, true);

      // Allowed: *.7u.pl and local environments
      const allowedOrigins = [
        /^https?:\/\/((.*)\.)?7u\.pl$/,
        /^http:\/\/localhost(:\d+)?$/,
        /^http:\/\/127\.0\.0\.1(:\d+)?$/
      ];

      if (allowedOrigins.some((regex) => regex.test(origin))) {
        callback(null, true);
      } else {
        callback(new Error('Not allowed by CORS'));
      }
    }
  }
});

Thanks to using Regex instead of rigid strings, domains such as sklep.7u.pl, as well as developer ports (e.g., localhost:5173 used by the Vite ecosystem) are authorized dynamically, without the need to restart and reconfigure the server for every new project.

Deployment and Maintenance (Docker)

The server weighs just a few megabytes. The Docker image was built based on the node:20-alpine environment.

dockerfile
FROM node:20-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install --only=production
COPY . .
EXPOSE 80
CMD ["npm", "start"]

On the production machine, the image is hidden behind a load balancer (NGINX/Cloudflare), which handles the ultimate TLS/SSL certificate termination (wss://sock.7u.pl). Such a setup means that the new infrastructure has become completely invisible ("transparent") for future modifications.

Summary and Benefits

Moving the WebSocket turn-around point ("relay") to a separate repository freed us from:

  1. Having backends forced to listen to ports just so "frontend-only" clients could see each other,
  2. Re-creating authentication systems from scratch,
  3. Maintaining unnecessary technical debt in projects.

From now on, every upcoming interactive project under the *.7u.pl domain will receive Real-Time functionality after adding five lines of import. And that's exactly what good architecture is all about.