running-nodejs-sidecar-in-tauri
Guides users through running Node.js as a sidecar process in Tauri applications, enabling JavaScript backend functionality without requiring end-user Node.js installations.
How do I install this agent skill?
npx skills add https://github.com/dchuk/claude-code-tauri-skills --skill running-nodejs-sidecar-in-tauriIs this agent skill safe to install?
- Gen Agent Trust Hubpass
This skill provides a comprehensive guide for integrating Node.js sidecars into Tauri applications. It follows official patterns for the Tauri Shell plugin and includes specific security recommendations, such as using argument validators and binding local servers to localhost. No malicious patterns or security risks were identified.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
- Runlayerfail
1/1 file flagged
What does this agent skill do?
Running Node.js as a Sidecar in Tauri
Package and run Node.js applications as sidecar processes in Tauri desktop applications, leveraging the Node.js ecosystem without requiring users to install Node.js.
Why Use a Node.js Sidecar
- Bundle existing Node.js tools and libraries with your Tauri application
- No external Node.js runtime dependency for end users
- Leverage npm packages that have no Rust equivalent
- Isolate Node.js logic from the main Tauri process
- Cross-platform support (Windows, macOS, Linux)
Prerequisites
- Existing Tauri v2 application
- Shell plugin installed and configured
- Node.js and npm on the development machine
- Rust toolchain (1.84.0+ recommended)
Install the Shell Plugin
npm install @tauri-apps/plugin-shell
cargo add tauri-plugin-shell --manifest-path src-tauri/Cargo.toml
Register in src-tauri/src/lib.rs:
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Project Structure
my-tauri-app/
├── package.json
├── src-tauri/
│ ├── binaries/
│ │ └── my-sidecar-<target-triple>[.exe]
│ ├── capabilities/default.json
│ ├── tauri.conf.json
│ └── src/lib.rs
├── sidecar/
│ ├── package.json
│ ├── index.js
│ └── rename.js
└── src/
Step-by-Step Setup
1. Create the Sidecar Directory
mkdir sidecar && cd sidecar
npm init -y
npm add @yao-pkg/pkg --save-dev
2. Write Sidecar Logic
Create sidecar/index.js:
const command = process.argv[2];
const args = process.argv.slice(3);
switch (command) {
case 'hello':
console.log(`Hello ${args[0] || 'World'}!`);
break;
case 'add':
const [a, b] = args.map(Number);
if (isNaN(a) || isNaN(b)) {
console.error('Error: Both arguments must be numbers');
process.exit(1);
}
console.log(JSON.stringify({ result: a + b }));
break;
default:
console.error(`Unknown command: ${command}`);
process.exit(1);
}
3. Create the Rename Script
Create sidecar/rename.js:
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
const ext = process.platform === 'win32' ? '.exe' : '';
let targetTriple;
try {
targetTriple = execSync('rustc --print host-tuple').toString().trim();
} catch {
const rustInfo = execSync('rustc -vV').toString();
const match = rustInfo.match(/host: (.+)/);
targetTriple = match ? match[1] : null;
if (!targetTriple) {
console.error('Could not determine Rust target triple');
process.exit(1);
}
}
const destDir = path.join('..', 'src-tauri', 'binaries');
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
fs.renameSync(`my-sidecar${ext}`, path.join(destDir, `my-sidecar-${targetTriple}${ext}`));
4. Configure Build Scripts
Update sidecar/package.json:
{
"name": "my-sidecar",
"type": "module",
"scripts": {
"build": "pkg index.js --output my-sidecar --targets node18",
"postbuild": "node rename.js"
},
"devDependencies": {
"@yao-pkg/pkg": "^5.0.0"
}
}
5. Configure Tauri
Add to src-tauri/tauri.conf.json:
{
"bundle": {
"externalBin": ["binaries/my-sidecar"]
}
}
6. Configure Permissions
Update src-tauri/capabilities/default.json:
{
"identifier": "default",
"windows": ["main"],
"permissions": [
"core:default",
{
"identifier": "shell:allow-execute",
"allow": [{
"args": true,
"name": "binaries/my-sidecar",
"sidecar": true
}]
}
]
}
Restrict arguments for security:
{
"identifier": "shell:allow-execute",
"allow": [
{
"args": ["hello", { "validator": "\\w+" }],
"name": "binaries/my-sidecar",
"sidecar": true
}
]
}
Communication Patterns
Frontend to Sidecar (TypeScript)
import { Command } from '@tauri-apps/plugin-shell';
async function sayHello(name: string): Promise<string> {
const command = Command.sidecar('binaries/my-sidecar', ['hello', name]);
const output = await command.execute();
if (output.code !== 0) throw new Error(output.stderr);
return output.stdout.trim();
}
async function addNumbers(a: number, b: number): Promise<number> {
const command = Command.sidecar('binaries/my-sidecar', ['add', String(a), String(b)]);
const output = await command.execute();
if (output.code !== 0) throw new Error(output.stderr);
return JSON.parse(output.stdout).result;
}
Backend to Sidecar (Rust)
use tauri_plugin_shell::ShellExt;
#[tauri::command]
async fn call_sidecar(
app: tauri::AppHandle,
command: String,
args: Vec<String>,
) -> Result<String, String> {
let mut sidecar = app.shell().sidecar("my-sidecar").map_err(|e| e.to_string())?;
sidecar = sidecar.arg(&command);
for arg in args {
sidecar = sidecar.arg(&arg);
}
let output = sidecar.output().await.map_err(|e| e.to_string())?;
if output.status.success() {
String::from_utf8(output.stdout).map_err(|e| e.to_string())
} else {
Err(String::from_utf8_lossy(&output.stderr).to_string())
}
}
Register the command:
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![call_sidecar])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Streaming Output
import { Command } from '@tauri-apps/plugin-shell';
async function runWithStreaming(args: string[]): Promise<void> {
const command = Command.sidecar('binaries/my-sidecar', args);
command.on('close', (data) => console.log(`Finished: ${data.code}`));
command.on('error', (error) => console.error(`Error: ${error}`));
command.stdout.on('data', (line) => console.log(`stdout: ${line}`));
command.stderr.on('data', (line) => console.error(`stderr: ${line}`));
await command.spawn();
}
Long-Running HTTP Sidecar
For persistent processes, use HTTP:
sidecar/index.js:
import http from 'http';
const PORT = process.env.SIDECAR_PORT || 3333;
const server = http.createServer((req, res) => {
let body = '';
req.on('data', (chunk) => (body += chunk));
req.on('end', () => {
res.setHeader('Content-Type', 'application/json');
try {
const data = body ? JSON.parse(body) : {};
if (req.url === '/hello') {
res.end(JSON.stringify({ message: `Hello ${data.name || 'World'}!` }));
} else if (req.url === '/health') {
res.end(JSON.stringify({ status: 'ok' }));
} else {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'Not found' }));
}
} catch (err) {
res.statusCode = 400;
res.end(JSON.stringify({ error: err.message }));
}
});
});
server.listen(PORT, '127.0.0.1', () => console.log(`Listening on ${PORT}`));
process.on('SIGTERM', () => server.close(() => process.exit(0)));
Frontend communication:
import { Command } from '@tauri-apps/plugin-shell';
import { fetch } from '@tauri-apps/plugin-http';
let sidecarProcess: any = null;
const PORT = 3333;
async function startSidecar(): Promise<void> {
if (sidecarProcess) return;
const command = Command.sidecar('binaries/my-sidecar', [], {
env: { SIDECAR_PORT: String(PORT) },
});
sidecarProcess = await command.spawn();
for (let i = 0; i < 10; i++) {
try {
const res = await fetch(`http://127.0.0.1:${PORT}/health`);
if (res.ok) return;
} catch {
await new Promise((r) => setTimeout(r, 100));
}
}
throw new Error('Sidecar failed to start');
}
async function callSidecar(endpoint: string, data?: object): Promise<any> {
const res = await fetch(`http://127.0.0.1:${PORT}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: data ? JSON.stringify(data) : undefined,
});
return res.json();
}
async function stopSidecar(): Promise<void> {
if (sidecarProcess) {
await sidecarProcess.kill();
sidecarProcess = null;
}
}
Building for Production
Update root package.json:
{
"scripts": {
"build:sidecar": "cd sidecar && npm run build",
"dev": "npm run build:sidecar && tauri dev",
"build": "npm run build:sidecar && tauri build"
}
}
Cross-platform targets:
| Platform | pkg Target | Rust Triple |
|---|---|---|
| Windows x64 | node18-win-x64 | x86_64-pc-windows-msvc |
| macOS x64 | node18-macos-x64 | x86_64-apple-darwin |
| macOS ARM | node18-macos-arm64 | aarch64-apple-darwin |
| Linux x64 | node18-linux-x64 | x86_64-unknown-linux-gnu |
Security
- Use validators instead of
"args": true - Bind HTTP servers to
127.0.0.1only - Validate input in both Tauri and sidecar
- Ensure sidecars terminate when the app closes
Troubleshooting
Binary not found: Check target triple matches:
ls -la src-tauri/binaries/
rustc --print host-tuple
Permission denied (Unix):
chmod +x src-tauri/binaries/my-sidecar-*
Silent crashes: Check stderr:
const output = await command.execute();
if (output.code !== 0) console.error(output.stderr);
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/dchuk/claude-code-tauri-skills/running-nodejs-sidecar-in-tauri">View running-nodejs-sidecar-in-tauri on skillZs</a>