Node-RED nodes for Dapr pub/sub, service invocation, state management, dynamic configuration, output bindings, and secrets, talking to a Dapr sidecar over its HTTP API only, with optional first-party OpenTelemetry tracing and application-log export.
- Node.js >= 22.9 (Node-RED 5's own minimum). Tested on 22, 24, and 26.
- Node-RED
>=5.0.1 <6. - A Dapr sidecar (
daprd) reachable from this app, and this app reachable from that sidecar — see Listener topology below.
From the Node-RED editor: menu → Manage palette → Install, then search for
dapr and pick @pauldeng/node-red-contrib-dapr-http.
Or from your Node-RED user directory (typically ~/.node-red):
npm install @pauldeng/node-red-contrib-dapr-httpThen restart Node-RED. The nodes below appear in the palette under
network; dapr-connection is a config node, created from another node's
own dialog rather than dragged from the palette.
| Node | Purpose |
|---|---|
dapr-connection |
Shared config node: one sidecar connection. |
dapr-publish |
Publish a message to a pub/sub topic. |
dapr-subscribe |
Receive messages for a pub/sub topic, with CEL routing rules and optional bulk delivery. |
dapr-ack |
Explicitly acknowledge a subscribed message (SUCCESS/RETRY/DROP). |
dapr-invoke |
Call a method on another Dapr app-id. |
dapr-service |
Expose a method other Dapr apps can invoke. |
dapr-response |
Reply to a dapr-service invocation. |
dapr-state |
Get, save, delete, bulk get, or transactionally update state-store values. |
dapr-config-get |
Read one or more values from a configuration store. |
dapr-config-subscribe |
Watch configuration keys and emit a message whenever any of them change. |
dapr-binding-out |
Invoke an output binding with a component-specific operation. |
dapr-secret-get |
Read one scoped secret from a secret store. |
Supported Dapr building blocks, over HTTP only. Every call to the sidecar is
a plain node:http request; the only runtime dependencies are the pinned
official OpenTelemetry packages used by optional tracing and application-log
export:
- Pub/sub — publish (single and bulk), subscribe (CEL routing rules, bulk delivery, raw payloads, dead-letter topics), and explicit SUCCESS/RETRY/DROP acknowledgement.
- Service invocation — calling another app-id's method, and exposing methods other apps can call.
- State management — get, save, delete, bulk get, and transaction, with ETag-based optimistic concurrency. Dapr's state query API is not supported: it remains alpha and depends on store-specific query capabilities.
- Dynamic configuration — a one-shot get, and a long-lived subscribe with its own connecting/subscribed/retrying/failed state, driven by the same sidecar health signal every other node already uses. A subscription emits the current values once it connects, retires its previous subscription after a health recovery, and then resumes change delivery with a fresh subscription.
- Output bindings — invoke a binding component with an explicit, component-specific operation. Input bindings are not covered: daprd probes input-binding app routes at startup, so wiring one up needs a sidecar restart and a lifecycle design of its own.
- Scoped secret retrieval — one secret at a time, by store and key. Bulk secret retrieval is not exposed: Dapr's own bulk endpoint applies secret-scoping per returned name and silently drops denied entries rather than failing, an unnecessarily large accidental-disclosure surface for a Debug node or message branch. This node does not automatically attach the key or value to its status, generated errors, or secret boundary span, and never forwards daprd's response text. The user-authored node name appears in generic flow telemetry, so keep secret names out of that name. daprd itself still receives the key in the request path; daprd's own logs and traces can contain the secret key, so configure those sinks as sensitive data too. The returned value is part of the flow message: do not send it to Debug or application logs.
Deliberately not covered: input bindings, bulk secrets, actors,
workflows, distributed lock, jobs, the conversation API, and gRPC transport.
A flow that needs one of those can reach it with a dapr-invoke node or a
core http request node against the sidecar's own API in the meantime.
Tracing is disabled by default. Enable Tracing on any dapr-connection to
export Dapr publish, subscribe, invoke, service, state, configuration,
output-binding, and secret-get boundary spans (the secret-get span carries
only the store name, never the secret's own key) plus spans for the
Node-RED nodes traversed by each message.
The integration propagates
W3C traceparent and tracestate automatically and uses standard OTEL_*
environment variables for service identity, the OTLP/HTTP collector endpoint,
authentication, and sampling. If Node-RED already owns a global OpenTelemetry
provider, the nodes reuse it without replacing or shutting it down.
Export is batched and off the message path; an unavailable collector never changes message delivery. See Telemetry for the lifecycle and span model.
A separate opt-in, installed through Node-RED's own settings.js
logging configuration rather than a node or connection field — that keeps
exporter credentials out of flow JSON and lets it capture Node-RED's own
startup logs before any flow deploys:
// settings.js
module.exports = {
// ...
logging: {
console: { level: 'info' }, // unaffected; keep or drop independently
otel: {
level: 'info', // Node-RED's own gating: fatal/error/warn/info/debug/trace
metrics: false, // opt in to export RED.log metric() events too
audit: false, // opt in to export audit events too
handler: require('@pauldeng/node-red-contrib-dapr-http/logging'),
},
},
};Every node.warn()/node.error()/etc. call and Node-RED's own runtime logs
export over OTLP/HTTP using the same standard OTEL_* environment variables
as tracing, and reuse the same resource identity — but logging is an
independent lease: enabling it never enables tracing or flow spans, and
disabling or redeploying a traced connection never stops it. A log emitted
from inside a traced node's own handler carries that span's trace and span
IDs automatically; an out-of-flow log (including everything logged before the
first flow deploys) has neither. Only a bounded, safe subset of each log
entry is exported — node id/type/name, flow id, level, a bounded message body,
and bounded exception details for an Error. The bridge never inspects or
automatically attaches a flow message, payload, Dapr token, request header, or
arbitrary object shape. Log text is still application-authored: explicitly
logging a secret string exports that string, so flows must not log secrets.
OTEL_LOGS_EXPORTER supports otlp (the default) and none;
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT and OTEL_EXPORTER_OTLP_LOGS_HEADERS
configure the OTLP/HTTP destination, and OTEL_BLRP_* controls the bounded
batch queue, batch size, schedule delay, and export timeout. OTEL_SDK_DISABLED
also disables export. Records produced during asynchronous provider startup use
a bounded startup buffer and retain their original Node-RED timestamp and
active trace context.
The OpenTelemetry JavaScript Logs API/SDK is still classified as Development, so its packages are pinned exactly and are upgraded only with the complete log contract passing.
This lease is process-lifetime: nothing in the ordinary deploy/redeploy path
releases it. An operator managing their own graceful shutdown can call
require('@pauldeng/node-red-contrib-dapr-http/logging').shutdown() to flush
and release it deliberately.
Import examples/basic-pubsub.json (menu → Import → paste or select the
file) for the smallest complete flow: publish a message, receive it back on
the same topic. examples/README.md has the NATS JetStream + daprd
quickstart. The example flows are in examples/:
basic-pubsub.json— publish and subscribe.cel-routing.json— subscribe with CEL routing rules.bulk-acknowledgement.json— bulk subscribe with per-entry acknowledgement.outbound-invocation.json— call another app-id's method.inbound-service.json— expose a method and reply withdapr-response.state-management.json— save a value to a state store, then read it back.dynamic-configuration.json— read and watch a dynamic configuration value.output-binding.json— invoke an output binding and inspect its response.secret-get.json— read one scoped secret into a message property.
examples/nats-jetstream-pubsub-component.yaml is the beginner pub/sub
component. examples/memorydb-pubsub-component.yaml is for AWS MemoryDB
operators. examples/redis-statestore-component.yaml backs
state-management.json. These are Dapr components, not flows: the nodes
never see the broker or store, so a flow works against any component of the
matching name/type unchanged. See docs/deployment.md.
The JetStream stream must exist before the component is used; see
docs/deployment.md for the repository's provisioning command. With the
pinned Dapr 1.18.2 runtime, do not use deadLetterTopic with NATS JetStream:
the real integration test observes delivery stall before the dead-letter
message arrives. Use Redis or broker-side dead-letter handling instead;
details are in docs/subscriptions.md.
Each dapr-connection node owns two independent HTTP paths, configured on
the same config node:
- Outbound (this app → sidecar): the sidecar's own HTTP API. Leave Dapr
host/port blank to use the sidecar's
DAPR_HTTP_ENDPOINTenvironment discovery; otherwise they default to127.0.0.1:3500. - Inbound app channel (sidecar → this app): a small, dedicated
node:httplistener this node starts itself for subscription discovery, pub/sub delivery, and service methods. It is never attached to the Node-RED editor port or Admin API — the Dapr mesh can never reach Node-RED's own flow-deploy endpoints through it. Point the sidecar's--app-portflag at this node's app port (default3000). The bind address defaults to127.0.0.1; only change it to something non-loopback if the sidecar runs on a different host, and pair that with an app API token (see below) plus network-level access controls, since a non-loopback bind exposes the listener beyond this host.
For daprd running as a separate container, the simplest topology puts daprd
in Node-RED's own network namespace (Docker's network_mode: service:<name>
or --network host) so both processes can reach each other over 127.0.0.1.
See docs/deployment.md for worked examples.
A Test Connection button on the dapr-connection dialog calls the
sidecar's own GET /v1.0/metadata and shows the app id, runtime version, the
first 20 loaded component names/types, and total component and subscription
counts — it tests the deployed connection, not whatever is currently typed
in the still-open dialog. Only that bounded set is shown; arbitrary extended
metadata, the sidecar's raw response, an API token, and raw error text never
reach the editor.
Two independent tokens, each set either as a credential on the
dapr-connection node or via an environment variable:
-
Dapr API token (credential, else
DAPR_API_TOKEN) — optional; sent on outbound calls to the sidecar, if the sidecar requires one. -
App API token (credential, else
APP_API_TOKEN) — enforced on every inbound request except/healthzwhen it is set. It authenticates the sidecar to the app; it does not authorize a calling application.If it is not set, the app channel authenticates nobody: anything that can reach the listener can read the subscription list and post deliveries into flows. That is allowed only on a loopback bind (and the connection node warns on every deploy); a non-loopback bind without a token is rejected and the listener does not start. Set it for anything past local development.
Two independent Dapr mechanisms, for two independent things — neither configured by this package's nodes:
- Service invocation (
dapr-invoke/dapr-service): Dapr'sspec.accessControlpolicy authorizes which app-id may call a method, but only works when mTLS is enabled between sidecars — without it, daprd can't read a caller's identity from a client certificate and evaluates every caller asid: "". This package does not stand up mTLS/Sentry, soaccessControlis not currently a usable caller-authorization mechanism with a default deployment. Use network-level controls instead until mTLS is configured. - Pub/sub (
dapr-publish/dapr-subscribe): topic access is controlled by the pubsub component's ownsubscriptionScopes,publishingScopes, andprotectedTopicsmetadata (set in the component YAML, not by this package) — a completely separate mechanism fromaccessControlthat does not need mTLS.
See docs/security.md for the full picture.
docs/invariants.md— the rules this package will not break, and why.docs/architecture.md— how the pieces fit together.docs/security.md— tokens, ACLs, and the mTLS gap in detail.docs/deployment.md— container/sidecar topology recipes.docs/subscriptions.md— when a subscription change needs a sidecar restart, and how to do it safely.docs/development.md— setup, commands, and the commit workflow.docs/testing.md— the test tiers and what each one covers.AGENTS.md— the durable engineering guide this project follows.