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.
open source · MIT · four repositories
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.
ls -1 ~/node-bridge
On each platform the work ships as two packages — a small bridge (API + loader, its own semver) and a runtime (engine only, versioned exactly like Node.js). They meet only at process runtime.
The Kotlin API and JNI bootstrap: the NodeRuntime state machine, the JSON-speaking host
bridge and a NAPI addon. Loads libnode.so at runtime — never links it.
io.github.fellow99:node-bridge:0.2.0
View repository
The Node.js engine for Android, packaged as an AAR: a cross-compiled
libnode.so (arm64-v8a) plus libc++_shared.so, nothing else.
The artifact version equals the Node.js version.
io.github.fellow99:node-runtime:24.2.0
View repository
The ArkTS API and NAPI bootstrap for HarmonyOS, published to ohpm: NodeRuntime,
HostBridge (Node → ArkTS), logging and crash forensics, and the dlopen loader.
@fellow99/node-bridge@0.2.0
View repository
The OpenHarmony arm64-v8a engine in a HAR: libnode.so +
libc++_shared.so and version constants. Cross-compiled with the
OpenHarmony LLVM-19 toolchain in cloud CI. No ArkTS API of its own.
@fellow99/node-runtime@24.2.0
View repository
nm -D libnode_bridge.so | grep Start
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 + dlsymlibnode_host_client.soNAPI addon, loaded via process.dlopenlibnode.soNode.js + V8, from the runtime packagebootstrap.js · host.jsJS payload, materialized from app assets
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.
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.
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.
Idle → Starting → Running → Exited/Failed is an observable state machine.
Hard platform limits are reported to the caller — never papered over.
./quickstart --platform
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.
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")
}
// 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
ohpm install @fellow99/node-bridge
ohpm install @fellow99/node-runtime@24.2.0
# install both — the bridge does not depend on the runtime
ohos.permission.kernel.ALLOW_WRITABLE_CODE_MEMORY ACL in your
module.json5. Without it --jitless still runs, but
WebAssembly is unavailable.
import { NodeRuntime } from '@fellow99/node-bridge';
NodeRuntime.getInstance().start(ctx, {
scriptPath: '<sandbox>/payload/bootstrap.js',
});
node::Start runs once per process; a crash is terminal. Use a dedicated :node process for restartability.2in1 / tablet only (no phone); Android needs minSdk 24. arm64-v8a is the only ABI.git clone --recurse-submodules
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.