Node.js runtime
Two constraints are specific to the Node runtime: where packages resolve from, and what happens to your
listen() call.
The shared package set
Bare require() calls resolve against a package set installed on the host and shared
by every application on the fleet. It is LockFlare’s, it is fixed, and an application process cannot write to
it.
require() for a package outside the set will not resolve at runtime.
The engine detects this and tells you exactly what is missing rather than failing with a bare module error. On startup it walks the application’s declared dependencies and logs any that will not resolve:
!! app example.com ("Kiero") — missing npm packages on the fleet
!! app fix: npm install stripe ioredis (on the fleet node, then restart lf-engine)
Node built-ins are skipped, and so are dev-only tools like nodemon — only real
runtime dependencies are reported.
Two ways to fix it
Install on the fleet node
What the engine recommends. Install into the shared set on the server, then restart the engine.
The package is then available to every application on that host — convenient on a single-tenant box, and worth thinking about if it is not.
Bundle into the artifact
Bundle at build time and the dependency travels inside the encrypted artifact, loading from the in-memory file map like your own modules.
Nothing to install on the server, and nothing shared with other applications.
cd /var/www/interpreter npm install stripe ioredis sudo systemctl restart lf-engine
npx esbuild server.js \ --bundle \ --platform=node \ --target=node24 \ --outfile=dist/server.js
Why it works this way: an application running from memory has no package directory of its own, so the resolver has to be provided by the host rather than built per application. The .NET runtime has no equivalent constraint, because NuGet dependencies travel inside the encrypted artifact.
Your listen() call is intercepted
The application loads into an isolated vm context, and its
listen() call is intercepted rather than honored — no socket is bound. The
runtime captures your handler and dispatches requests into it directly, routing by
Host header.
For a normal application this is invisible. You write the same code you always would:
const express = require('express')
const app = express()
app.get('/', (req, res) => res.send('hello'))
// Called as normal. The runtime captures app and
// dispatches into it; no socket is actually bound.
app.listen(3000)
| Module | What the runtime does |
|---|---|
express, koa, fastify | Shimmed. .listen() is captured per context; your handler is dispatched into directly. |
http, https | Shimmed the same way, so createServer(handler).listen() works. |
fs | Shimmed for reads — see below. |
cluster | Shimmed. Your primary/worker code runs against an in-process bus, not real IPC. |
os | Shimmed. os.cpus() reports your licensed worker count, not the host’s physical cores. |
dotenv, dotenvx | Shimmed to a no-op. Your uploaded .env is already parsed and injected before your code runs, so config().parsed returns it. |
| Everything else | Resolved normally against the fleet package set. |
Reading files
fs reads are served from your bundled tree first, so route auto-loaders,
template directories and asset reads work as written. Beyond that there are two behaviours worth knowing.
| Read | Result |
|---|---|
| A path inside your bundle | Served from the in-memory map. |
| A relative path not in your bundle | ENOENT. Relative paths never fall through to the engine’s own directory. |
| An absolute path on the host | Read normally — the real filesystem. |
The engine’s own .env | Always ENOENT. Your application cannot read the fleet licence. |
fs.writeFileSync('./uploads/x.jpg') succeeds — it writes into the engine’s
working directory — but reading it back by the same relative path returns ENOENT,
because relative reads resolve against your bundle. Anything written there is also outside your deployment and
gone when the server is rebuilt. Use object storage or a database; see
Both runtimes.
Workers and cores
The runtime forks clustered workers against the core count licensed to that server — four cores, four workers. Anything your application holds in process memory is per worker, not shared across them.
os.cpus() reports your licensed worker count rather than the machine’s, so
code that sizes a pool from it gets the right answer. But in-memory session stores, in-process caches and rate
limiters that assume a single process will still behave inconsistently across workers. This is the ordinary clustered-Node caveat rather than a LockFlare one, and the
ordinary answer applies: move shared state to Redis or the database.
Isolation is language-level
Applications run in separate vm contexts within one runtime process — not
separate OS processes with separate users and namespaces, which is what the .NET runtime provides.
That is a property of the JavaScript runtime, not something LockFlare introduced. It is the right boundary when the code on a host is all yours. Where applications on one host belong to different customers or different trust domains, use the .NET runtime or give each trust domain its own server.