How to Set Up WebSocket Communication Between a Chrome Extension and a Node.js Server

Last summer, I worked on a user-adaptive dark mode system in our lab.

The project eventually got shelved — but the architecture I designed back then taught me a lot about server-driven UI control and real-time communication between backend and browser…


This content originally appeared on DEV Community and was authored by easy1nhard2

Last summer, I worked on a user-adaptive dark mode system in our lab.

The project eventually got shelved — but the architecture I designed back then taught me a lot about server-driven UI control and real-time communication between backend and browser extensions.

In this post, I’ll share how I designed a system that allows the server to directly modify the client’s UI in real-time using WebSocket, and why the traditional HTTP request-response model was insufficient.


1. The Limitations of HTTP and the Need for WebSocket

In traditional web communication, HTTP follows a strict one-way flow:

Client sends request → Server responds → Connection ends.

This works fine for typical API calls, but not for cases where the server must actively control or update the client’s state — for example, a system that adjusts the browser’s brightness or contrast based on user conditions.

Imagine this:
The server decides that “the user’s screen is too bright” and wants to lower it immediately.
With HTTP, the server can’t act first — it has to wait for the client to make a request.

That’s where WebSocket comes in.

What is WebSocket?

WebSocket is a full-duplex communication protocol that keeps a persistent connection between the client and the server.
After an initial handshake, both sides can freely send and receive data in real time.

Feature HTTP WebSocket
Connection Ends after each request Persistent after handshake
Direction One-way (client → server) Two-way (client ↔ server)
Typical Use APIs, static data Chat, alerts, real-time control
Protocol HTTP/HTTPS ws:// or wss://

With WebSocket, the server can initiate actions — sending commands like “increase font size” or “reduce brightness” to the client instantly.

That’s exactly what my adaptive dark mode system needed.


2. System Overview and Implementation

2.1 Architecture Concept

The system had to be server-driven — meaning, the server sends instructions to modify the client UI dynamically.

To implement this, I used:

  • Node.js (Express + ws) for the backend
  • Chrome Extension (TypeScript) as the client

The Chrome Extension was based on the Dark Reader project, modified to accept WebSocket-based UI control commands from the backend.

Through many trials (and a few CSP headaches), I found that managing the WebSocket connection in background.ts was the most stable and secure approach.

2.2 Why background.ts?

A Chrome Extension typically has three main components:

Component Description Lifecycle
popup The UI window that opens when clicking the extension icon Temporary (closes when UI closes)
content script Injected into web pages to manipulate DOM Reloads with page
background Runs persistently in the background and manages app logic Persistent

Since WebSocket requires a continuous connection, placing it in popup or content script would cause it to disconnect whenever the UI closes or a page refreshes.

Moreover, Chrome Extensions have strict Content Security Policy (CSP) restrictions.
The background script (a service worker) is the only stable place to maintain a persistent WebSocket connection and relay messages between the server, popup, and content scripts.

2.3 Basic System Design

[ Chrome Extension ]
    └─► background.ts
          └─ WebSocket connection
          └─ Send/receive messages with server

[ Node.js Backend Server ]
    └─ WebSocket server (ws://localhost:3001)
          └─ Handle incoming messages and send responses

Backend Setup

npm install
npm install ws
npm install express

Since only the basic structure has been built so far, I’ve written the initial WebSocket handling logic inside the index.js file.

Later, you can start the server simply by running the command:

node index.js

Once the server starts successfully, the waiting WebSocket server will also be activated.

Server Implementation

The following code demonstrates a simple structure where the server receives messages from connected clients and sends the same content back in an echo format.

index.js (server-side)

const WebSocket = require('ws');
const PORT = 3001;
const wss = new WebSocket.Server({ port: PORT });

wss.on('connection', (ws) => {
  console.log('✅ Client connected via WebSocket');

  ws.on('message', (message) => {
    console.log('📨 Client message:', message.toString());
    ws.send(`Server echo: ${message}`);
  });

  ws.on('close', () => {
    console.log('❌ Client disconnected');
  });
});

console.log(`🟢 WebSocket Server running at ws://localhost:${PORT}`);

This simple echo server lays the groundwork for future adaptive commands like “update brightness” or “change contrast.”

Client Implementation (background.ts)

The following code implements the WebSocket client within the Chrome Extension’s background.ts file.

We modified version 3.5.4 of the dark mode extension Dark Reader to include this functionality.
The code below shows the portion that was added to the extension’s background.ts file.

Because attempting to connect directly from the popup is blocked by Content Security Policy (CSP) restrictions, we designed the background service worker, which runs continuously, to handle the persistent connection with the server.

let socket: any = null;

function connectWebSocket() {
  socket = new WebSocket('ws://localhost:3001');

  socket.onopen = () => {
    console.log('[bg] WebSocket connected');
    socket.send('[bg] Extension connected to server');
  };

  socket.onmessage = (event) => {
    console.log('[bg] Server response:', event.data);
    chrome.runtime.sendMessage({ from: 'server', data: event.data });
  };

  socket.onclose = () => {
    console.warn('[bg] Connection closed — retrying...');
    setTimeout(connectWebSocket, 3000);
  };

  socket.onerror = (err) => {
    console.error('[bg] WebSocket error:', err);
    socket.close();
  };
}

// Relay popup messages to the server
chrome.runtime.onMessage.addListener((msg) => {
  if (msg.from === 'popup' && socket?.readyState === 1) {
    socket.send(msg.data);
  }
});

// Initialize
loadConfigs(() => {
  extension = new DarkReader.Extension(new DarkReader.FilterCssGenerator());
  onExtensionLoaded.invoke(extension);
  connectWebSocket();
});

3. Running the System

⚠️ Note: Dark Reader 3.5.4 uses Manifest V2, which is deprecated in modern Chrome versions.
You may need to downgrade Chrome to test it.

3.1 Build the Extension

npm run release

If dependencies are missing, follow npm’s installation suggestions.

3.2 Load in Chrome

  1. Go to chrome://extensions
  2. Enable Developer Mode

  1. Click Load unpacked
  2. Select your unzipped build folder

You should see Dark Reader 3.5.4 appear in the list.

3.3 Run the Backend

node index.js

You should see:

🟢 WebSocket Server running (ws://localhost:3001)
✅ Client connected

In Chrome’s background console(html), you’ll find logs like:

[bg] WebSocket connected
[bg] Server response: [bg] Extension connected to server

Connection success!

4. Adding Dynamic Dark Mode Control

Finally, I added a fun test: every 5 seconds, the server sends random dark mode settings to the extension, which are then applied dynamically.

Updated index.js

const WebSocket = require('ws');
const PORT = 3001;
const wss = new WebSocket.Server({ port: PORT });

wss.on('connection', (ws) => {
  console.log('✅ Client connected');

  ws.on('message', (message) => {
    console.log('📨 Received:', message.toString());
    ws.send(`Server echo: ${message}`);
  });

  const intervalId = setInterval(() => {
    const data = {
      type: 'UPDATE_FILTER',
      payload: {
        mode: 1,
        brightness: Math.floor(Math.random() * 100) + 50,
        contrast: Math.floor(Math.random() * 100) + 50,
        grayscale: Math.floor(Math.random() * 100),
        sepia: Math.floor(Math.random() * 100),
        useFont: Math.random() > 0.5,
        fontFamily: "Open Sans",
        textStroke: Math.floor(Math.random() * 3),
        invertListed: false,
        siteList: []
      }
    };
    console.log('Server sending:', data);
    ws.send(JSON.stringify(data));
  }, 5000);

  ws.on('close', () => {
    console.log('❌ Client disconnected');
    clearInterval(intervalId);
  });
});

console.log(`🟢 WebSocket Server running at ws://localhost:${PORT}`);

Updated background.ts

socket.onmessage = (event) => {
  try {
    const data = JSON.parse(event.data);
    if (data.type === 'UPDATE_FILTER') {
      console.log('[bg] Received UPDATE_FILTER:', data.payload);
      for (const key in data.payload) {
        if (extension.config.hasOwnProperty(key)) {
          extension.config[key] = data.payload[key];
        }
      }
      chrome.tabs.query({}, (tabs) => {
        tabs.forEach(tab => {
          if (tab.id && tab.url) {
            extension["addStyleToTab"](tab);
          }
        });
      });
    }
  } catch (e) {
    console.log('[bg] Server message:', event.data);
  }
};

After running this, you can watch your extension’s theme update dynamically every 5 seconds — brightness, contrast, grayscale, and more.



✅ Conclusion

This project was a valuable experiment in real-time UI adaptation using WebSocket communication between a Node.js backend and a Chrome Extension client.

Key takeaways:

  • HTTP alone isn’t enough for server-driven UIs.
  • WebSocket enables true bi-directional, event-based communication.
  • The background script is the right place to manage persistent connections in Chrome Extensions.

Even though the project never went into production, it laid the groundwork for building adaptive, responsive, and context-aware browser systems — where the server doesn’t just respond, but actively orchestrates the client experience.


This content originally appeared on DEV Community and was authored by easy1nhard2


Print Share Comment Cite Upload Translate Updates
APA

easy1nhard2 | Sciencx (2025-10-05T12:57:29+00:00) How to Set Up WebSocket Communication Between a Chrome Extension and a Node.js Server. Retrieved from https://www.scien.cx/2025/10/05/how-to-set-up-websocket-communication-between-a-chrome-extension-and-a-node-js-server/

MLA
" » How to Set Up WebSocket Communication Between a Chrome Extension and a Node.js Server." easy1nhard2 | Sciencx - Sunday October 5, 2025, https://www.scien.cx/2025/10/05/how-to-set-up-websocket-communication-between-a-chrome-extension-and-a-node-js-server/
HARVARD
easy1nhard2 | Sciencx Sunday October 5, 2025 » How to Set Up WebSocket Communication Between a Chrome Extension and a Node.js Server., viewed ,<https://www.scien.cx/2025/10/05/how-to-set-up-websocket-communication-between-a-chrome-extension-and-a-node-js-server/>
VANCOUVER
easy1nhard2 | Sciencx - » How to Set Up WebSocket Communication Between a Chrome Extension and a Node.js Server. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/10/05/how-to-set-up-websocket-communication-between-a-chrome-extension-and-a-node-js-server/
CHICAGO
" » How to Set Up WebSocket Communication Between a Chrome Extension and a Node.js Server." easy1nhard2 | Sciencx - Accessed . https://www.scien.cx/2025/10/05/how-to-set-up-websocket-communication-between-a-chrome-extension-and-a-node-js-server/
IEEE
" » How to Set Up WebSocket Communication Between a Chrome Extension and a Node.js Server." easy1nhard2 | Sciencx [Online]. Available: https://www.scien.cx/2025/10/05/how-to-set-up-websocket-communication-between-a-chrome-extension-and-a-node-js-server/. [Accessed: ]
rf:citation
» How to Set Up WebSocket Communication Between a Chrome Extension and a Node.js Server | easy1nhard2 | Sciencx | https://www.scien.cx/2025/10/05/how-to-set-up-websocket-communication-between-a-chrome-extension-and-a-node-js-server/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.