Bare: a JavaScript runtime that treats embedding and phones as the normal case
Small and modular JavaScript runtime for desktop and mobile
At a glance
- What is it?
- Bare is a small JavaScript runtime written in C for desktop and mobile, built on libjs and libuv, where the runtime itself supplies only a module system, native addons and light weight threads, and everything else is a userland npm package. It is aimed at applications that live inside someone else's app, which is why suspension, sealing and host loop polling are first class features rather than afterthoughts.
- Who is it for?
- Adopt Bare if you are building a JavaScript runtime inside a host application, a mobile app with a scriptable core, or a peer to peer application that has to run on a phone as well as a laptop, and if you are willing to build against a C API rather than a Node compatible one. Do not adopt it as a drop in Node replacement for a server application, because the runtime deliberately omits the Node built in modules and expects them to be installed as userland packages.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The pitch: like Node, but embeddable and phone-shaped
Bare is described as a small and modular JavaScript runtime for desktop and mobile. Like Node.js, it provides an asynchronous, event driven architecture for writing applications in what the README calls the lingua franca of modern software. Unlike Node.js, it makes embedding and cross device support core use cases, aiming to run just as well on your phone as on your laptop. The result is described as a runtime ideal for networked, peer to peer applications that can run on a wide selection of hardware. Two things follow from that positioning. First, the audience is not the web server developer. It is the developer who needs a JavaScript engine inside a host process, an app with a scriptable extension layer, or a device where the whole runtime has to be small enough to ship to a phone. Second, the surface area is deliberately different from Node's. The JavaScript API is available through a global `Bare` namespace and documented in `src/bare.d.ts`, and the package manifest for the runtime itself is private with an npm workspace, so the install path is a single global install of the runtime binary.
Two foundations and only three additions
The architecture section is short because the design is short. Bare is built on top of libjs, which provides low level bindings to V8 in an engine independent manner, and on libuv, which provides the asynchronous I/O event loop. On top of those two, Bare itself adds only three things. A module system supporting both CJS and ESM with bidirectional interoperability between the two. A native addon system supporting both statically and dynamically linked addons. And light weight threads with synchronous joins and `SharedArrayBuffer` support. Everything else, the README says, is left to userland modules to implement using these primitives, keeping the runtime itself succinct and bare. The payoff of that approach is stated precisely: by abstracting over the JavaScript engine with libjs and over platform I/O with libuv, Bare allows module authors to implement native addons that run on any JavaScript engine implementing the libjs ABI and on any system libuv supports. The cost is equally precise, and the npm manifest shows it. The runtime's own dependencies are userland packages, including bare-buffer, bare-bundle, bare-console, bare-events, bare-hrtime, bare-inspector, bare-module, bare-module-resolve, bare-os, bare-path, bare-pipe, bare-queue-microtask, bare-repl, bare-signals, bare-structured-clone, bare-timers, bare-tty and bare-url, plus paparam. There is no filesystem or HTTP in the runtime itself; those appear as dev dependencies here, bare-fs and bare-http1, and as installable modules elsewhere.
Embedding from C, and the host loop problem
Bare can be embedded using the C API defined in `include/bare.h`, and the shape of a host program is short. You set the runtime up against a libuv loop, load a module, run the loop, and tear down:
#include <bare.h>
#include <uv.h>
bare_t *bare;
bare_setup(uv_default_loop(), platform, &env /* Optional */, argc, argv, options, &bare);
bare_load(bare, filename, source, &module /* Optional */);
bare_run(bare, UV_RUN_DEFAULT);
int exit_code;
bare_teardown(bare, UV_RUN_DEFAULT, &exit_code);If `source` is `NULL`, the contents of `filename` are read at runtime instead, which lets a host decide when a script is loaded. The interesting case is a host whose thread belongs to a host loop, such as the run loop of a user interface, where the host drives the loop with `bare_poll()` rather than `bare_run()`, and `bare_poll` runs the loop without blocking and reports how long the host may sleep before calling again. The README spells out a trap here worth repeating. A timeout of `-1` means the host may sleep until the backend descriptor of the loop, as given by `uv_backend_fd()`, becomes readable, and a host that sleeps on the timeout alone rather than on the descriptor will miss work that arrives from another thread. That is the kind of detail that costs a day of debugging if nobody wrote it down.
Attaching, detaching, and a per process context
Native code in a runtime that may run on several threads needs a clear rule about which thread it is on, and Bare has one. `bare_run()` attaches the process to the thread while it runs, and so does loading an addon, so native code reached through either is already attached. A call the embedder makes itself is not, so the embedder attaches the process around it with `bare_attach()` and detaches with `bare_detach()`. The README notes that attachments nest, that `bare_attach()` hands back the process that was attached before so that `bare_detach()` can attach it again, and that detaching out of order returns `-1` and restores nothing. Attaching does not run the loop, and the call may leave work behind that only the loop will run, which is why the example runs the loop after detaching. On top of that sits a context mechanism for values only the embedder has, such as a `JavaVM *` on Android. Values are set per process with `bare_context_set()`, cannot be changed once set, should all be set before the first call to `bare_load()`, and addons read them with `bare_context_get()`. The README is explicit about the security consequence in a note: any value that is set is available to every addon in the process.
Suspension exists because mobile lifecycles demand it
The suspension mechanism is the clearest signal of what kind of runtime this is. Bare provides a way to implement process suspension, which the README says is needed for platforms with strict application lifecycle constraints, such as mobile platforms. When the process is suspended, using `bare_suspend()` from C or `Bare.suspend()` from JavaScript, a `suspend` event is emitted on the `Bare` namespace. Then, when the loop has no work left and would otherwise exit, an `idle` event is emitted and the loop blocked, keeping it from exiting. When the process is later resumed with `bare_resume()`, a `resume` event is emitted and the loop unblocked, allowing it to exit when no work is left. There is a third state, a limited wakeup. Using `bare_wakeup()` or `Bare.wakeup()` emits a `wakeup` event, and each wakeup has an associated deadline after which the loop is stopped and the process suspended again, emitting another `idle` event. A desktop server runtime has no equivalent requirement, which is exactly why this mechanism is unusual to find and why it tells you more about Bare's intended host than any feature list would.
The security story is a documented threat model, not a promise
Bare is designed to be embedded alongside code the embedder may not fully trust, and the mechanism for that is `Bare.Addon.seal()`, which freezes the set of native code a process may load. The README is careful about what this does and does not promise, and points at `docs/threat-model.md`, which it says embedders should read before running untrusted JavaScript. That framing is the right one, because native addons are the hole in any sandbox: an addon can do anything the host process can, so the only real defence is limiting which ones load. The context mechanism has the same flavour, and the README repeats the warning in a note, that any value set is available to every addon in the process, with a pointer back to the threat model. Sealing also has a side effect worth knowing, since it prevents setting any further context values. For an embedder that runs downloaded or user supplied code, the practical sequence is therefore to set every context value first, then load the module, then seal, and to read the threat model before deciding that this is enough.
Building it, and the pace of the releases
Compilation is handled by a separate tool, bare-make, which is installed globally with `npm i -g bare-make`, after which the repository dependencies are installed with `npm i` and the build system is generated with `bare-make generate`. The README says that generation only has to be run once per repository checkout, though updating bare-make or the compiler toolchain may require running it again with the `--no-cache` flag to disregard the existing build system cache, and the binary is produced by `bare-make build`, with the `bare(.exe)` binary available afterwards. The repository is a CMake project, with `CMakeLists.txt` at the root and cmake helper packages in the dev dependencies such as cmake-bare, cmake-bare-bundle, cmake-drive, cmake-fetch, cmake-harden and cmake-napi, alongside a `test/` directory, a `bench/` directory, an `include/` directory for the C header and a `docs/` directory. The release pace is fast: v1.35.0 and v1.35.1 were both published on 2026-10-13, v1.34.1 a fortnight earlier on 2026-09-30, and the last push was on the same day as the newest tag. The licence is Apache-2.0, with a NOTICE file at the root, and the npm package is private, since the runtime is distributed as a binary rather than as a library dependency.
Editorial conclusion
Adopt Bare if you are building a JavaScript runtime inside a host application, a mobile app with a scriptable core, or a peer to peer application that has to run on a phone as well as a laptop, and if you are willing to build against a C API rather than a Node compatible one. Do not adopt it as a drop in Node replacement for a server application, because the runtime deliberately omits the Node built in modules and expects them to be installed as userland packages. Before you start, read `docs/threat-model.md`, since the addon sealing and the process wide context values have consequences for untrusted code, and check whether the bare-* npm packages your application needs exist for the platform you target. The licence is Apache-2.0, releases v1.35.0 and v1.35.1 both landed on 2026-10-13, and the last push was the same day.
Frequently asked questions
What is Bare and how is it different from Node.js?
Bare is a small and modular JavaScript runtime for desktop and mobile. Like Node.js it provides an asynchronous, event driven architecture, but unlike Node.js it makes embedding and cross device support core use cases, and it ships a C API for hosts to embed the runtime.
What does Bare itself provide on top of libjs and libuv?
Only three things: a module system supporting both CJS and ESM with bidirectional interoperability, a native addon system supporting statically and dynamically linked addons, and light weight threads with synchronous joins and `SharedArrayBuffer` support. Everything else is left to userland modules.
How do I embed Bare in a C application?
Include `bare.h` and `uv.h`, then call `bare_setup()` with a libuv loop, `bare_load()` to load a module, `bare_run()` to run the loop and `bare_teardown()` to shut down. A host with its own event loop should use `bare_poll()` instead of `bare_run()`, and can call `bare_attach()` and `bare_detach()` around calls the embedder makes itself.
How do I run untrusted JavaScript in Bare?
The README says Bare is designed to be embedded alongside code the embedder may not fully trust, and that `Bare.Addon.seal()` freezes the set of native code a process may load. It points to `docs/threat-model.md` and says embedders should read it before running untrusted JavaScript, and notes that any context value set is available to every addon in the process.
Why does Bare need a suspension mechanism?
Because platforms with strict application lifecycle constraints, such as mobile platforms, require it. `bare_suspend()` or `Bare.suspend()` emits a suspend event, the loop emits an idle event and blocks instead of exiting, and `bare_resume()` unblocks it; a limited `bare_wakeup()` wakes the loop for a bounded period with a deadline.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/holepunchto-bare)Was this useful?