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:

1

Create two folders: libs/ and ninesdklibs/.

2

Copy the nineproxysdk module into libs/nineproxysdk.

Point your app’s package.json dependency to that local path:

{
  "dependencies": {
    "nineproxysdk": "./libs/nineproxysdk"
  }
}
3

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

Tip: Re-run this after upgrading Electron or changing architectures.

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?