ElectronJS
This guide shows how to integrate 9PROXY SDK (Node-API addon / Native) into an Electron application with a secure, layered architecture. It uses the SDK’s internal WebSocket channel and synchronous/asynchronous APIs to manage proxies and ports.
Architecture Overview
Goal: integrate 9PROXY SDK into Electron using safe separation of concerns:
Main Process (Node): load the SDK addon, start the main loop (runAsync), configure the SDK, open the internal WebSocket channel, and expose APIs to the renderer via IPC.
Preload (Bridge with contextIsolation: true): create a safe window.sdk bridge that forwards calls over IPC.
Renderer (UI/FE): call
window.sdk.*
methods and listen to SDK events (forwarded via IPC, or connect directly if needed).
Preparation & Rebuild the Addon for Electron
Install Electron and essential tooling:electron, electron-builder (or forge), electron-rebuild.
Install packages
// install
# Install Electron + builder
npm i --save electron electron-builder
# (Optional) Official tutorial
# https://www.electronjs.org/docs/latest/tutorial/tutorial-first-app
# Install 9PROXYSDK
npm i --save nineproxysdk
Place native SDK libraries
Create a folder (e.g. ninesdklibs/) that you will pass to sdk.initialize(''). Copy all native libraries into it:
// path folder
ninesdklibs/
ninesdk.dll
libninesdk.dylib
libninesdk_arm.so
libninesdk_amd64.so
This directory must be packaged with your app and remain unpacked at runtime (see electron-builder config later).
Alternative: manual vendoring
If you prefer to vendor the module inside your repo:
Create two folders: libs/ and ninesdklibs/.
Copy the nineproxysdk module into libs/nineproxysdk.
Point your app’s package.json dependency to that local path:
{
"dependencies": {
"nineproxysdk": "./libs/nineproxysdk"
}
}
Copy the native libraries into ninesdklibs/:
ninesdklibs/
ninesdk.dll
libninesdk.dylib
libninesdk_arm.so
libninesdk_amd64.so
You’ll still call sdk.initialize('ninesdklibs') using this folder name.
Rebuild native modules for Electron
If your Electron version uses a different Node ABI than your development Node, rebuild:
npx electron-rebuild
electron-builder Configuration (Reference)
Configure electron-builder so the native libs are unpacked from the ASAR and available at runtime.
electron-builder.yml:
asar: true
asarUnpack:
- "ninesdklibs/**"
- "resources/**"
files:
- "dist/**"
- "ninesdklibs/**"
- "resources/**"
NOTE
asar: true keeps your app bundled; asarUnpack ensures native binaries (.node, .dll, .so, .dylib) are extracted and loadable.
Make sure the folder name you pass to sdk.initialize('') matches the on-disk path you ship (e.g. ninesdklibs).
Main Process: SDK Integration & IPC
Importing modules
// import modules
const sdk = require('nineproxysdk');
//Next, use sdk.initialize and sdk.runAsync to start the SDK
sdk.initialize('ninesdklibs');
sdk.runAsync();
// Here 'ninesdklibs' is the path to the folder containing the native SDK libraries,
// as described in the setup section above
// Use sdk.enableDebug to turn on debug logging for the SDK
// (recommended only in development builds)
sdk.enableDebug();
Set the storage path for SDK data
const userDataDir = app.getPath('userData');
const saveFilePath = path.join(userDataDir, 'ninesdk_data.dat');
// In this example, saveFilePath is where the SDK stores its data.
// It keeps user forward-port information here, so that it can be restored
// after the app is closed.
// To make this work, pass the path into the SDK using sdk.setSaveDataPath.
// Later, you can call sdk.restoreData to reload the saved state.
sdk.setSaveDataPath(saveFilePath);
Set the API Key
sdk.setAPIHost('https://g-api-dev.9proxy.com/sdk/v1');
sdk.setQuery('api-key', process.env.SDK_API_KEY || '');
// APIHost is the base URL of the 9Proxy SDK API.
// For better security and easier management of your IP usage / API keys,
// you can forward this API through your own backend service instead.
// After that, just configure setAPIHost, setQuery, and setHeader
// according to your backend’s rules. For example:
sdk.setAPIHost('https://yourbackend.com/ninesdk');
sdk.setHeader('Authorization', 'Bearer …');
Configure port range and IP format
sdk.updateIpFormatAndPorts('127.0.0.1:%d', 60000, 20);
// The port format and IP must follow a valid pattern that the app can use.
// Examples: "0.0.0.0:%d", "127.0.0.1:%d", or the user’s LAN IP + ":%d".
//
// Also note: start port + limit must not exceed the OS port range (maximum 65535).
Set up a WS channel for SDK events
const wsPort = 7301;
const token = 'abcd1234';
sdk.startEventListener(wsPort, token, callbackMessage);
// wsPort should be a port that is guaranteed not to conflict with
// other apps or with proxies configured above.
//
// The token is used to authenticate with the SDK WebSocket,
// ensuring that no external applications can access it.
Restore previous session data
sdk.restoreData();
// It’s best to call restoreData only after the WebSocket connection
// has been successfully established. This ensures that you’ll receive
// the correct restore events.
sdk.startEventListener(wsPort, token, callbackMessage).then(() => {
sdk.restoreData();
});
Set up IPC methods to be called from the renderer process
ipcMain.handle('sdk:get-api-host', () => sdk.getAPIHost());
ipcMain.handle('sdk:set-api-host', (_, host) => sdk.setAPIHost(host));
ipcMain.handle('sdk:quick-create-at-port', (_, port, reqId, '1') =>
callOrThrow(sdk.quickCreateAtPort, port, reqId, '1')
);
ipcMain.handle('sdk:get-config-json', (_, port) => sdk.getConfigJSON(port));
ipcMain.handle('sdk:get-ports', () => sdk.getPorts());
// ….
// In the current SDK version, only residential proxy IP plans are supported.
// That’s why plan = '1' is used as the default.
Final result of main.js
// main.js
const { app, BrowserWindow, ipcMain } = require('electron/main')
const path = require('node:path')
const sdk = require('nineproxysdk');
var win = null
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
}
})
win.loadFile('index.html')
}
app.on('ready', () => {
win = createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
win = createWindow()
}
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
app.on('before-quit', () => {
try { sdk.stopCommunicate(); } catch {}
try { sdk.shutdown(); } catch {}
});
function callbackMessage(data) {
try {
const msg = JSON.parse(data.toString());
win?.webContents.send('sdk:event', msg);
} catch {
win?.webContents.send('sdk:event', { type: 'raw', payload: data.toString() });
}
}
const userDataDir = app.getPath('userData');
const saveFilePath = path.join(userDataDir, 'ninesdk_data.dat');
// initialize SDK
sdk.initialize('ninesdklibs');
sdk.runAsync();
sdk.enableDebug();
sdk.setSaveDataPath(saveFilePath);
sdk.setAPIHost('https://g-api-dev.9proxy.com/sdk/v1');
sdk.setQuery('api-key', process.env.SDK_API_KEY || '');
// update IP format + port range
sdk.updateIpFormatAndPorts('127.0.0.1:%d', 60000, 20);
// open internal ws channel
const wsPort = 7301;
const token = 'abcd1234'; // TODO: random & save to memory
sdk.startEventListener(wsPort, token, callbackMessage).then(()=> {
// restore older data
sdk.restoreData();
})
function callOrThrow(fn, ...args){
const rc = fn(...args);
if (typeof rc === 'number' && rc !== 0){
throw new Error(sdk.sdkGetLastError?.() || `SDK error rc=${rc}`);
}
return rc;
}
// IPC handlers
ipcMain.handle('sdk:get-version', () => sdk.getVersionInfo());
ipcMain.handle('sdk:get-api-host', () => sdk.getAPIHost());
ipcMain.handle('sdk:quick-create-at-port', (_, port, reqId, plan) =>
callOrThrow(sdk.quickCreateAtPort, port, reqId, plan));
ipcMain.handle('sdk:get-config-json', (_, port) => sdk.getConfigJSON(port));
ipcMain.handle('sdk:get-configs-json', (_, port) => sdk.getConfigsJSON());
ipcMain.handle('ping', () => 'pong')
Preload: bridge an toàn
Từ các hàm được đăng ký sử dụng thông qua ipc của electron tại preload ta định nghĩa các hàm này thành js function để đơn giản hoá + an toàn trong việc truy vấn
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('sdk', {
getApiHost: () => ipcRenderer.invoke('sdk:get-api-host'),
quickCreateAtPort: (port, reqId, plan) => ipcRenderer.invoke('sdk:quick-create-at-port', port, reqId, plan),
getConfigJSON: (port) => ipcRenderer.invoke('sdk:get-config-json', port),
getConfigsJSON: () => ipcRenderer.invoke('sdk:get-configs-json'),
onEvent: (cb) => {
const listener = (_, msg) => cb(msg);
ipcRenderer.on('sdk:event', listener);
return () => ipcRenderer.removeListener('sdk:event', listener);
}
});
Renderer (UI): sử dụng bridge & lắng nghe event
Trên giao diện sử dụng 'DOMContentLoaded' để lắng nghe các event mà sdk giử lên thông qua icp
// renderer.js
document.addEventListener('DOMContentLoaded', async () => {
// Listen for events from the SDK
const off = window.sdk.onEvent((msg) => {
switch (msg.type) {
case 'restoreDone':
console.log('Restored!');
break;
case 'settingPortChange': {
const [start, count] = String(msg.payload || '').split(':').map(Number);
console.log('New range', start, count);
break;
}
case 'forwardListCompleted':
console.log('Forward done for req:', msg.payload);
break;
case 'proxyError':
console.error('Proxy error:', msg.payload);
break;
case 'portStateUpdate':
console.log('Port updated:', msg.payload);
break;
case 'portRemoved':
console.log('Port removed:', msg.payload);
break;
case 'log':
console.debug('SDK log:', msg.payload);
break;
case 'httpError':
console.error('HTTP error:', msg.payload);
break;
default:
console.log('Event:', msg);
}
});
// Example: call an SDK API
const host = await window.sdk.getApiHost();
console.log('Current host:', host);
// Example: forward a single port
// await window.sdk.quickCreateAtPort(60000, 'request_id_example', '1');
// Clean up when leaving the page
// off();
});
Security & Packaging
Enable hardening: contextIsolation: true, nodeIntegration: false.
Before exit: call stopCommunicate() and shutdown() to cleanly stop SDK tasks.
Integration Checklist
Rebuild native addons to match Electron’s ABI (electron-rebuild).
Configure asarUnpack for addon.node and all native libraries.
Use Preload + IPC; do not expose Node directly in the Renderer.
Hide tokens/secrets; proxy SDK API via your backend when possible.
Manage SDK lifecycle: start runAsync(), open WS, restoreData(), and on quit call stopCommunicate() + shutdown().
Last updated
Was this helpful?