Architecture
How the edgelinkd process is organised, and how a message travels from one node to the next.
EdgeLinkd is a single native process. The same binary hosts the flow engine and the Node-RED editor with its admin API — or, in headless mode, the engine alone.
Workspace layout
| Path | Purpose |
|---|---|
src/ | The edgelinkd binary: CLI (run, list), configuration, logging, web or headless mode |
crates/core/ | edgelink-core — engine, flows, nodes, context, message model, JavaScript bridge |
crates/web/ | edgelink-web — admin HTTP API and editor hosting, built on axum |
crates/macro/ | #[flow_node] and #[global_node] procedural macros for self-registration |
crates/pymod/ | edgelink_pymod — the Python extension driven by the test suite |
node-plugins/ | Statically linked node plug-ins |
tests/ | pytest port of Node-RED's mocha specification suite |
3rd-party/node-red/ | Pinned Node-RED checkout (v4.1.15): editor assets and behavioural reference |
Runtime model
The engine owns the deployed flows. When a flows.json is deployed — from the editor or from disk — the engine:
- parses the configuration into flows, subflows, groups and nodes;
- resolves every node
typeagainst the node registry; - starts one Tokio task per node, each with a bounded input channel;
- connects every output port to the inboxes of the nodes it is wired to.
Each node processes one message at a time, in arrival order — the ordering contract Node-RED flows rely on. Because the channels are bounded, a slow consumer exerts back-pressure on its producers instead of letting queues grow without limit, so memory use stays predictable on small devices.
Every node task receives a cancellation token; on redeploy or shutdown the running tasks are stopped through it before a new graph starts.
Messages
A message is a Msg: a set of properties whose values are Variants, a JSON-like dynamic value type. Messages travel between tasks as shared MsgHandles — Arc<RwLock<Msg>> — and are forwarded through per-port wire senders.
Nodes
A node is a Rust type implementing FlowNodeBehavior. It registers itself with the #[flow_node] macro and the inventory crate, so there is no central list to maintain: adding a node means adding a module.
Abridged from crates/core/src/runtime/nodes/common_nodes/junction.rs:
#[flow_node("junction", red_name = "junction")]
struct JunctionNode {
base: BaseFlowNodeState,
}
#[async_trait]
impl FlowNodeBehavior for JunctionNode {
fn get_base(&self) -> &BaseFlowNodeState {
&self.base
}
async fn run(self: Arc<Self>, stop_token: CancellationToken) {
while !stop_token.is_cancelled() {
let cancel = stop_token.child_token();
// Take one message from the inbox, forward it to output port 0.
with_uow(self.as_ref(), cancel.child_token(), |node, msg| async move {
node.fan_out_one(Envelope { port: 0, msg }, cancel.child_token()).await?;
Ok(())
})
.await;
}
}
}
A small build() constructor, omitted here, is what the registry calls when a flow containing the node is deployed. Implementations are grouped by palette category under crates/core/src/runtime/nodes/: common, function, network, sequence, parser and storage.
Plug-ins
Third-party nodes live in node-plugins/ and are statically linked. Tokio async functions cannot call into dynamically loaded libraries, so dynamic plug-ins are not offered; plug-ins based on WebAssembly or JavaScript are being evaluated for the future.
Scripting and expressions
- JavaScript runs only in
functionnodes, inside an embedded QuickJS engine (throughrquickjs). The sandbox exposesnode,context,flow,global,envand theRED.utilsurface. There is no Node.jsBufferand norequire(). - JSONata is evaluated by the pure-Rust
jsonata-coreengine — forchange,switchandinjectproperties as well as environment variables.
Context
Node, flow and global context are backed by context stores: an in-memory store and a local file-system store.
Web layer
edgelink-web serves the Node-RED editor, the admin HTTP API the editor talks to, and the channel that feeds the debug sidebar. It binds to 127.0.0.1:1888 by default; --headless skips it entirely.
Build profile and features
The release profile optimises for size (opt-level = "z", LTO, one codegen unit, stripped symbols). Runtime facilities and node families are Cargo features — core, js, jsonata, nodes_network, nodes_storage, nodes_parser — so every dependency has to justify its binary-size cost, and minimal builds can leave whole families out.
Verification
Compatibility is tested, not assumed. Node-RED's mocha specs are ported to pytest with identical describe() and it() titles and run against the real engine through the edgelink_pymod PyO3 extension. The live coverage page shows the generated comparison by version and node.