A WebSocket server is designed for receiving real-time data, unlike a classic web server where you open a link in your browser and get data (e.g., html, xml, json, etc.). With a classic server, it establishes a connection -> transmits data -> terminates the connection.
A WebSocket server works differently: you establish a connection with it, and it remains continuously active. As soon as data changes, the server transmits it to the client. Because it uses bidirectional communication, the connection is always active and is not terminated.
1. Application
To receive real-time data, for example, if you want to display your website visitors or online store order statuses live on a separate web page or even on a separate device.
2. How it Works
A WebSocket server is a separate service that runs on a dedicated port you assign for subsequent connections. The WS (WebSocket server) or WSS (WebSocket server + SSL) protocol allows you to configure real-time data output, for example, via a browser using html + JS.
Please note! Since the service requires opening a new port, setting up and running such a service is only possible on a virtual server. This is precisely why a WebSocket server is unavailable for virtual hosting services.
Imagine if every virtual hosting client wanted to run such a service – a separate port would need to be opened for each, which would compromise the security of the service itself. Regardless of the hosting provider, setup and configuration are only possible on a VPS/VDS server.
3. Example Task
Suppose we have a PHP script https://domain.tld/online-orders.php that generates data output in JSON format, and we want this data to be displayed in real-time. Currently, achieving this requires constant refreshing.
Please note, while for a PC and browser, data can be updated by increasing the request frequency, in the case of frequent requests and JSON data updates from a microcontroller, it would simply cause it to hang for 1-2 seconds. And, say, a regular POST/GET request every 20 seconds is still not truly real-time.
Therefore, we need WSS. Let's launch a new VPS on Debian 12 with a minimal resource configuration. High performance is not needed for WSS. All configurations will be performed as the root user.
3.1 Service Installation
apt update
apt install -y curl git nodejs npm
3.2 Creating the WebSocket Project
mkdir ~/websocket-server
cd ~/websocket-server
npm init -y
npm install ws node-fetch express
3.3 Create a configuration file in the websocket-server project root
nano server.js
import express from "express";
import { WebSocketServer } from "ws";
import fetch from "node-fetch";
import https from "https";
import fs from "fs";
// === Settings ===
const PORT = 8080; // Choose a port for this WebSocket server
const FETCH_URL = "https://domain.tld/online-orders.php";
const FETCH_INTERVAL = 1000; // once every 1 second
// === HTTPS certificates ===
// (If WSS is needed, an SSL certificate is required. You can use Let's Encrypt or self-signed)
const options = {
key: fs.readFileSync("/etc/ssl/private/your-key.pem"),
cert: fs.readFileSync("/etc/ssl/certs/your-cert.pem")
};
// === HTTP(S) server + WebSocket ===
const app = express();
const server = https.createServer(options, app);
const wss = new WebSocketServer({ server });
let latestData = null;
// === Periodic JSON request ===
async function updateData() {
try {
const res = await fetch(FETCH_URL);
const json = await res.json();
latestData = json;
// Send to all connected clients
wss.clients.forEach(client => {
if (client.readyState === 1) { // WebSocket.OPEN
client.send(JSON.stringify(json));
}
});
} catch (err) {
console.error("Error fetching data:", err);
}
}
setInterval(updateData, FETCH_INTERVAL);
updateData(); // initial load
// === WebSocket connection ===
wss.on("connection", ws => {
console.log("Client connected");
if (latestData) ws.send(JSON.stringify(latestData));
ws.on("close", () => console.log("Client disconnected"));
});
server.listen(PORT, () => {
console.log(`WSS server started on port ${PORT}`);
});
3.4 Configuring SSL for WSS via Let's Encrypt
For SSL, a domain is required. We will use a subdomain, for example, ws.domain.tld.
apt install certbot python3-certbot-nginx
certbot certonly --standalone -d ws.domain.tld
Follow the short procedure, providing an email address for notifications. Upon completion, the certificate and key will be here:
/etc/letsencrypt/live/ws.domain.tld/fullchain.pem
/etc/letsencrypt/live/ws.domain.tld/privkey.pem
Replace the paths in the server.js file:
const options = {
key: fs.readFileSync("/etc/letsencrypt/live/ws.domain.tld/privkey.pem"),
cert: fs.readFileSync("/etc/letsencrypt/live/ws.domain.tld/fullchain.pem")
};
3.5 Running and Debugging
node server.js
We received an error SyntaxError: Cannot use import statement outside a module.
In this case, you need to modify the package.json file by adding the line "type": "module",.
Example of correct package.json configuration:
{
"name": "websocket-server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
...
Run again:
node server.js
WSS server started on port 8080
Client connected
3.6 Configure Autostart:
npm install -g pm2
pm2 start server.js --name websocket
pm2 save
pm2 startup
3.7 Check Service Operation
pm2 list
3.8 Connect using HTML and JS
Create a file named wss-client.html on your PC with the following content:
You cannot connect to a WebSocket directly via a domain in a browser like a web server; a script needs to be created. Specify your domain instead of ws.domain.tld.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WSS Test</title>
<style>
body {
font-family: monospace;
background: #111;
color: #0f0;
padding: 20px;
}
input, button {
background: #222;
color: #0f0;
border: 1px solid #0f0;
padding: 6px 10px;
font-family: monospace;
}
#log {
margin-top: 20px;
white-space: pre-wrap;
background: #000;
border: 1px solid #0f0;
padding: 10px;
height: 400px;
overflow-y: auto;
}
</style>
</head>
<body>
<h2>🔌 WebSocket Test Client</h2>
<label>Server URL:</label>
<input id="wsUrl" type="text" size="50" value="wss://ws.domain.tld:8080">
<button onclick="connect()">Connect</button>
<button onclick="disconnect()">Disconnect</button>
<div id="status">Status: <b>Disconnected</b></div>
<div id="log"></div>
<script>
let ws;
function log(msg) {
const logDiv = document.getElementById('log');
logDiv.innerText += msg + "\n";
logDiv.scrollTop = logDiv.scrollHeight;
}
function connect() {
const url = document.getElementById('wsUrl').value;
ws = new WebSocket(url);
log("🔄 Connecting to " + url + " ...");
document.getElementById('status').innerHTML = "Status: <b>Connecting...</b>";
ws.onopen = () => {
log("✅ Connection established");
document.getElementById('status').innerHTML = "Status: <b style='color:lime'>Connected</b>";
};
ws.onmessage = event => {
try {
const json = JSON.parse(event.data);
log("📦 Received:\n" + JSON.stringify(json, null, 2));
} catch (e) {
log("📦 Text:\n" + event.data);
}
};
ws.onclose = () => {
log("❌ Connection closed");
document.getElementById('status').innerHTML = "Status: <b style='color:red'>Disconnected</b>";
};
ws.onerror = err => {
log("⚠️ Error: " + err.message);
};
}
function disconnect() {
if (ws) {
ws.close();
ws = null;
}
}
</script>
</body>
</html>
3.9 Viewing Logs
The connection should work; as soon as data changes in our script, the WebSocket updates it for all connected clients.
You can view the event log with the command:
pm2 logs websocket
0|websocket | 5 visits updated
4. Authorization
In the example above, anyone can connect to the WebSocket server, so let's add token-based authorization.
Updated server.js code:
import express from "express";
import { WebSocketServer } from "ws";
import fetch from "node-fetch";
import https from "https";
import fs from "fs";
import url from "url";
// === Settings ===
const PORT = 8080;
const FETCH_URL = "https://domain.tld/online-orders.php";
const FETCH_INTERVAL = 1000; // every 1 second
const VALID_TOKENS = ["ABC123", "ESP32TOKEN", "MYSECRET"]; // list of allowed tokens
// === SSL ===
const options = {
key: fs.readFileSync("/etc/letsencrypt/live/ws.domain.tld/privkey.pem"),
cert: fs.readFileSync("/etc/letsencrypt/live/ws.domain.tld/fullchain.pem")
};
// === Create server ===
const app = express();
const server = https.createServer(options, app);
const wss = new WebSocketServer({ server });
let latestData = null;
// === Function for periodic JSON fetching ===
async function updateData() {
try {
const res = await fetch(FETCH_URL);
const json = await res.json();
latestData = json;
// Send to all active clients
wss.clients.forEach(client => {
if (client.readyState === 1) { // WebSocket.OPEN
client.send(JSON.stringify(json));
}
});
} catch (err) {
console.error("Error fetching data:", err);
}
}
setInterval(updateData, FETCH_INTERVAL);
updateData();
// === Handle connections ===
wss.on("connection", (ws, req) => {
const query = url.parse(req.url, true).query;
const token = query.token;
if (!VALID_TOKENS.includes(token)) {
console.log(`❌ Connection rejected with token: ${token}`);
ws.close(4001, "Invalid token");
return;
}
console.log(`✅ Client connected with token: ${token}`);
// Send the latest state
if (latestData) ws.send(JSON.stringify(latestData));
ws.on("close", () => {
console.log(`🔌 Client (${token}) disconnected`);
});
});
server.listen(PORT, () => {
console.log(`✅ WSS server started on port ${PORT}`);
});
4.1 Connecting with Token Authorization
For an example using HTML + JS, simply change the line to:
<input id="wsUrl" value="wss://ws.domain.tld:8080/?token=ABC123">
5. In Conclusion
Setup is complete; now all WebSocket server clients receive real-time data using encryption. In our example, we use a PHP script as an intermediary between the system (to get data using an API) and the WebSocket server (to transmit it).
The PHP script itself can be removed, and an API request can be used directly in server.js with API authorization to the system, which would be even more efficient. However, the article would then become quite large, as it would be necessary to cover secure storage of the API key in an .env file and installing the npm install dotenv package for working with configuration file data.

