We have a feature at work that requires the web app to communicate to a native app on the user local machine. I find that Chrome extension can send native message to a native app installed on the user machine. Basically, the web app will send a message to the extension, which then sends it through to the native app.

For the extension, we need a background script which listens for requests from the web app.
// background.js
const NATIVE_HOST_NAME = 'some.sample.domain.name';
chrome.runtime.onMessageExternal.addListener((request, sender, sendResponse) => {
chrome.runtime.sendNativeMessage(NATIVE_HOST_NAME, request, sendResponse);
});
In the manifest.json file, we need to add “nativeMessaging” in the permissions.
{
"manifest_version": 2,
"name": "Sample Extension",
"version": "1.0.0",
"background": {
"scripts": [
"background.js"
],
"persistent": false
},
"permissions": [
"nativeMessaging"
],
"externally_connectable": {
"matches": [
"http://localhost:3000/*"
]
}
}
The externally_connectable is set to localhost so it will work for the web app running locally but you want to change it to your domain in production.
Then we need to install this extension in Chrome. Go to chrome://extensions/ and switch on developer mode. Then click Load Unpacked and select the folder where the extension is. You should then see something like this.

Copy the ID, we will need it later.
Next, we look into the native app that the extension talks to. For the native app, we’ll be building a .Net Core 3.1 console app. The console app will receive requests by standard input stream. The app is called NativeHost.
public class Program
{
public static void Main(string[] args)
{
using var stdin = Console.OpenStandardInput();
using var stdout = Console.OpenStandardOutput();
// First 4 bytes is payload length
var payloadLengthBuffer = new byte[4];
stdin.Read(payloadLengthBuffer, 0, payloadLengthBuffer.Length);
var length = BitConverter.ToUInt32(payloadLengthBuffer, 0);
var receivedPayloadBuffer = new byte[length];
stdin.Read(receivedPayloadBuffer, 0, (int)length);
var payload = Encoding.UTF8.GetString(receivedPayloadBuffer);
// Response cannot be just string, it crashes the extension
var response = new { text = $"Native app received request: {payload}" };
var responseBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(response));
// Write the payload length in 4 bytes
stdout.WriteByte((byte)((responseBytes.Length >> 0) & 0xFF));
stdout.WriteByte((byte)((responseBytes.Length >> 8) & 0xFF));
stdout.WriteByte((byte)((responseBytes.Length >> 16) & 0xFF));
stdout.WriteByte((byte)((responseBytes.Length >> 24) & 0xFF));
// Now the actual payload
stdout.Write(responseBytes, 0, responseBytes.Length);
stdout.Flush();
}
}
Next we need to register the native app using the following configuration NativeHostConfig.json
{
"name": "some.sample.domain.name",
"description": "Sample native app host",
"path": "native-host.bat",
"type": "stdio",
"allowed_origins": [
"chrome-extension://eklgpfdhpffgfecicfbikddafnclmnjf/"
]
}
Make sure the name matches the NATIVE_HOST_NAME from the background.js and allowed_origins matches your extension ID. We use a .bat script called native-host.bat to run the console app.
@echo off
"%~dp0\NativeHost.exe"
Both the native-host.bat and NativeHostConfig.json are set to always be copied to Output Directory.
Then we need to create registry key
HKEY_LOCAL_MACHINE\SOFTWARE\Google\Chrome\NativeMessagingHosts\some.sample.domain.name
or
HKEY_CURRENT_USER\SOFTWARE\Google\Chrome\NativeMessagingHosts\some.sample.domain.name
, if Chrome is installed only for the current user.
Next, set the default value of that key to the full path to the NativeHostConfig.json. Since I build the console app in debug mode, NativeHostConfig.json will be in the debug folder with the exe file. Note that in the real world, you should create an installer for that takes care of all this for the users.
Finally, for the web app, we’ll use the default React app from create-react-app command. I add a button which sends the message to the extension when clicked.
import React, { useState, useEffect } from "react";
import logo from './logo.svg';
import './App.css';
const EXTENSION_ID = 'eklgpfdhpffgfecicfbikddafnclmnjf';
function App() {
const [responseFromNativeApp, setResponseFromNativeApp] = useState("");
const handleOnclick = () => {
// The request cannot be just string, it crashes the extension
window.chrome.runtime.sendMessage(EXTENSION_ID, { text: 'hello from web app' }, (response) => {
if (response) {
setResponseFromNativeApp(response.text);
}
});
};
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
<h1>{responseFromNativeApp}</h1>
<button onClick={handleOnclick}>Send message to extension</button>
</header>
</div>
);
}
export default App;
And this is the result
