open source · MIT · four repositories

Run real Node.js inside your Android or HarmonyOS app.

Not a WebView, not a cloud function, not a subprocess. A JNI/NAPI bootstrap calls dlopen("libnode.so") + node::Start in your app's own process, with a bidirectional JSON bridge between JavaScript and Kotlin or ArkTS.

  • Android · minSdk 24
  • HarmonyOS · 2in1 / tablet
  • Node.js v24.2.0
  • arm64-v8a
  • 4repositories
  • 2mobile platforms
  • v24.2.0Node.js engine
  • 100%in-process

nm -D libnode_bridge.so | grep Start

One process. One engine. Two packages.

The bridge is just a loader and a JSON-speaking host. It never links the engine; it finds libnode.so at runtime with dlopen and enters it through node::Start(int, char**). The engine itself is an ordinary shared object — ET_DYN, no PT_INTERP.

The NAPI addon leaves its napi_* symbols undefined on purpose (-z undefs); they resolve against the already-loaded engine (RTLD_GLOBAL), so Node's own N-API implementation is always used. Swap Node versions without recompiling the bridge.

  • libnode_bridge.soloader — links only log/dl; dlopen + dlsym
  • libnode_host_client.soNAPI addon, loaded via process.dlopen
  • libnode.soNode.js + V8, from the runtime package
  • bootstrap.js · host.jsJS payload, materialized from app assets
in-process.txt
01

Contract, not linkage

The only required symbols are node::Start and the stable N-API surface. The bridge does not know or care which Node minor version provides them.

02

Two packages per platform

A ~half-MB bridge on its own semver, and a ~41 MB engine whose version is the Node version. Ship bridge fixes without touching the engine, and vice-versa.

03

Pinned, reproduced, verified

Every engine is CI-cross-compiled from the official Node tarball. SHA-256 is locked in runtime.lock.json; 7 ELF gates check ET_DYN, PT_INTERP=0, ABI and the exported entry point.

04

Failures stay visible

Idle → Starting → Running → Exited/Failed is an observable state machine. Hard platform limits are reported to the caller — never papered over.

./quickstart --platform

Add two dependencies, call start

The bridge gives you the API; the runtime supplies the engine. Both are required — the bridge never declares a hard dependency on a specific engine build.

Android

Maven Central
build.gradle.kts
dependencies {
    implementation("io.github.fellow99:node-bridge:0.2.0")
    // engine, pinned to the Node.js version
    runtimeOnly("io.github.fellow99:node-runtime:24.2.0")
}
NodeRuntime.kt
// register host capabilities FIRST, then start
val rt = NodeRuntime.getInstance(context)
rt.host.register("fs.pickFolder") {
    """{"ok":true,"path":"/sdcard/Download"}"""
}
rt.start(NodeRuntimeOptions(
    entry = "bootstrap.js",
    appEntry = "selfcheck.js",
))
// Idle → Starting → Running → Exited/Failed
Read the Android guide

HarmonyOS

ohpm
terminal
ohpm install @fellow99/node-bridge
ohpm install @fellow99/node-runtime@24.2.0
# install both — the bridge does not depend on the runtime
JIT permission: V8 JIT needs the ohos.permission.kernel.ALLOW_WRITABLE_CODE_MEMORY ACL in your module.json5. Without it --jitless still runs, but WebAssembly is unavailable.
NodeRuntime.ets
import { NodeRuntime } from '@fellow99/node-bridge';

NodeRuntime.getInstance().start(ctx, {
  scriptPath: '<sandbox>/payload/bootstrap.js',
});
Read the HarmonyOS guide

Honest constraints — reported, never silently degraded

  • node::Start runs once per process; a crash is terminal. Use a dedicated :node process for restartability.
  • HarmonyOS targets 2in1 / tablet only (no phone); Android needs minSdk 24. arm64-v8a is the only ABI.
  • App assets are not a filesystem path; the JS payload is materialized into the app sandbox before start — the library handles it.
  • JIT is verified on real hardware: JIT and WebAssembly confirmed working on Android 15 and HarmonyOS 2in1 devices.

git clone --recurse-submodules

Four repos. Zero black boxes.

Every engine binary is CI-built from the official Node.js source, checksum-locked and ELF-gated before publishing. The umbrella workspace wires all four up as submodules.