LockFlare

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.

You cannot add a dependency by pushing it. A bare 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:

journalctl -u lf-engine
!! 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.

install on the fleet node
cd /var/www/interpreter
npm install stripe ioredis
sudo systemctl restart lf-engine
or bundle at build time
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:

server.js — unchanged
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)

ModuleWhat the runtime does
express, koa, fastifyShimmed. .listen() is captured per context; your handler is dispatched into directly.
http, httpsShimmed the same way, so createServer(handler).listen() works.
fsShimmed for reads — see below.
clusterShimmed. Your primary/worker code runs against an in-process bus, not real IPC.
osShimmed. os.cpus() reports your licensed worker count, not the host’s physical cores.
dotenv, dotenvxShimmed to a no-op. Your uploaded .env is already parsed and injected before your code runs, so config().parsed returns it.
Everything elseResolved normally against the fleet package set.
The rule of thumb: if your application is a request handler with a framework around it, it works untouched. If it reaches past the framework to the socket itself, that part needs rethinking — no socket is bound.

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.

ReadResult
A path inside your bundleServed from the in-memory map.
A relative path not in your bundleENOENT. Relative paths never fall through to the engine’s own directory.
An absolute path on the hostRead normally — the real filesystem.
The engine’s own .envAlways ENOENT. Your application cannot read the fleet licence.
Writes are not intercepted, and that is a trap. 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.