No description
  • C++ 96.9%
  • Go 1.2%
  • CMake 1.1%
  • Shell 0.8%
Find a file
Jona Heinrichs 49b3034e91
Some checks failed
ci / linux (push) Failing after 41s
ci / sanitize (address) (push) Failing after 40s
ci / sanitize (thread) (push) Failing after 42s
ci / esp32 (push) Failing after 14s
ci / format (push) Has been skipped
Object download on a slow link: wait long enough for a chunk
labctl stores firmware in 128 KiB chunks. On a camera's slow WiFi one
chunk took longer than the 5 s JetStream timeout: the fetch expired, the
late chunk went to a closed subscription, the ordered consumer started
that chunk again, and the download never got past 0 % (a livelock). A
fetch now waits for its chunks at no less than 8 KiB/s (a 128 KiB chunk:
16 s), the silence limit is three such waits.

Regression test through a new test helper, a TCP proxy that throttles
the server-to-client direction (128 KiB/s, a 500 ms timeout): it failed
after 31 s before the fix, passes in ~3 s after.
2026-09-30 00:57:01 +02:00
.gitea/workflows Add architecture docs, contributor notes, CI and formatting config 2026-09-25 20:56:35 +02:00
core Options::task_stack_psram: reader and callback stacks in PSRAM 2026-09-29 21:05:55 +02:00
examples Rewrite as nats:: API modelled on nats.go, with tests 2026-09-25 19:09:51 +02:00
go/nkey Fix NKey public key, shrink build, add Linux support 2026-09-25 18:01:14 +02:00
js Support clients with narrow permissions 2026-09-28 19:29:34 +02:00
kv Fix bugs found while documenting, with regression tests 2026-09-25 20:08:15 +02:00
obj Object download on a slow link: wait long enough for a chunk 2026-09-30 00:57:01 +02:00
platform Options::task_stack_psram: reader and callback stacks in PSRAM 2026-09-29 21:05:55 +02:00
test Object download on a slow link: wait long enough for a chunk 2026-09-30 00:57:01 +02:00
third_party/monocypher Fix NKey public key, shrink build, add Linux support 2026-09-25 18:01:14 +02:00
transport Fix bugs found while documenting, with regression tests 2026-09-25 20:08:15 +02:00
update Esp32Updater: erase sector by sector while downloading 2026-09-30 00:30:54 +02:00
util Add architecture docs, contributor notes, CI and formatting config 2026-09-25 20:56:35 +02:00
.clang-format Add architecture docs, contributor notes, CI and formatting config 2026-09-25 20:56:35 +02:00
.editorconfig Add architecture docs, contributor notes, CI and formatting config 2026-09-25 20:56:35 +02:00
.gitignore Rewrite as nats:: API modelled on nats.go, with tests 2026-09-25 19:09:51 +02:00
AGENTS.md Add architecture docs, contributor notes, CI and formatting config 2026-09-25 20:56:35 +02:00
ARCHITECTURE.md Add architecture docs, contributor notes, CI and formatting config 2026-09-25 20:56:35 +02:00
CLAUDE.md Add architecture docs, contributor notes, CI and formatting config 2026-09-25 20:56:35 +02:00
CMakeLists.txt jwt: decode and verify NATS JWTs 2026-09-29 00:39:48 +02:00
idf_component.yml Rewrite as nats:: API modelled on nats.go, with tests 2026-09-25 19:09:51 +02:00
nats.hpp Document the library with Doxygen (Javadoc style) comments 2026-09-25 19:49:08 +02:00
README.md README: JWT verification 2026-09-29 00:43:14 +02:00

nats-cpp

A C++17 NATS client for ESP-IDF (ESP32) and Linux (including the Raspberry Pi), built from the same source. The API follows nats.go and its jetstream package:

  • Core NATS: publish/subscribe, request/reply, queue groups, synchronous subscriptions, headers, drain, and auto-reconnect with keepalive
  • Authentication: user/password, token, NKey, JWT, XKey and TLS (with optional mTLS)
  • JWT verification: nats::jwt::Decode checks a NATS JWT's signature against its issuer's NKey (for tokens other parties issue), and NKey::verify any Ed25519 signature
  • JetStream: streams, publish acks, async publishing, pull consumers (Fetch, Next, Consume) and ordered consumers
  • Key-Value store: get, put, create/update with compare-and-set, delete, purge, keys, history and watch
  • Object store: streaming put/get, SHA-256 verification, list and delete
  • OTA updates: on ESP32, straight from the object store

Data written by this client can be read by the Go client and the nats CLI, and the other way round. The test suite checks this in both directions.

More documentation:

  • ARCHITECTURE.md: how the library works inside (threads, lifetimes, locking, JetStream mechanics)
  • AGENTS.md: commands, rules and pitfalls, for contributors and AI assistants
  • The API reference is in the header comments. Generate HTML with Doxygen if you like.

Quick start

#include "nats.hpp"
using namespace std::chrono_literals;

auto nc = nats::Connect("nats://localhost:4222");
if (!nc) { printf("%s\n", nc.error().what()); return; }

nc->Subscribe("greet.*", [](const nats::Msg& m) { m.Respond("hi " + m.data); });
auto reply = nc->Request("greet.joe", "joe", 1s);          // Result<Msg>

nats::JetStream js(*nc);
auto kv = js.CreateKeyValue(nats::KeyValueConfig("config"));
kv->Put("wifi.ssid", "home");
auto e = kv->Get("wifi.ssid");                              // Result<KvEntry>
kv->Watch("wifi.>", [](const nats::KvEntry& e) { /* live updates */ });

auto os = js.CreateObjectStore(nats::ObjectStoreConfig("firmware"));
os->Get("app.bin", [](const uint8_t* p, size_t n) { return write_flash(p, n); });

For more, see examples/linux/ (pubsub, jetstream, kv, objstore) and examples/esp32/ (WiFi, KV-based config, OTA and telemetry).

Narrow permissions

Clients that may only use their own subjects (e.g. devices behind an auth callout) work as with nats.go:

  • Options::inbox_prefix (default _INBOX) for replies, when the server only allows <prefix>.> (nats.go CustomInboxPrefix).
  • Consumers with one filter subject are created with the filter in the API subject ($JS.API.CONSUMER.CREATE.<stream>.<name>.<filter>, server 2.9+).
  • The last message of a subject (KV Get, object info) is read with $JS.API.DIRECT.GET.<stream>.<subject>.

Ordered consumers are pull consumers, so such a client also needs $JS.API.CONSUMER.MSG.NEXT.<stream>.* (and DELETE for the cleanup). See test/integration/test_permissions.cpp for a device's full permission set.

Error handling

There are no exceptions, because ESP-IDF builds with -fno-exceptions. Every call returns a nats::Result<T>, or a nats::Status for calls that return no value:

auto e = kv->Get("key");
if (!e) {
    if (e.code() == nats::ErrorCode::NotFound) { /* missing or deleted */ }
    printf("%s\n", e.error().what());
} else {
    printf("%s\n", e->value.c_str());
}

The error codes include Timeout, NoResponders, NotConnected, NotFound, KeyExists, WrongRevision, DigestMismatch, Auth and Api. For Api, the JetStream err_code is in error().api_code.

Threads, callbacks and lifetime

  • Each connection has one reader thread and one callback thread. All message handlers, watch callbacks and Options::on_* callbacks run on the callback thread. Inside a callback you can call Request(), kv.Get(), js.Publish() and so on without deadlocking.
  • On ESP32, the stack sizes, priority and core are set in Options. Raise callback_stack_size if your callbacks need more stack; OTA needs about 8 KiB.
  • A connection closes when the last handle to it (Conn, JetStream, KeyValue, ...) is destroyed, or when you call Close()/Drain(). Consumers and watches keep running until Stop() is called or the connection closes.
  • Don't capture a handle by value in a callback of the same connection. The connection would then keep itself alive. Capture by reference, or call Close() explicitly.

ESP-IDF (idf.py / Espressif-IDE)

Put the library in components/nats-cpp. The folder must be called nats-cpp, because ESP-IDF uses the folder name as the component name. Or add it to your main/idf_component.yml:

dependencies:
  neto/nats-cpp: '*'
  # optional, enables ws:// and wss://
  # espressif/esp_websocket_client: "^1.6.1"

For tls://, the ESP-IDF certificate bundle is used unless you set Options::tls_ca_pem.

OTA from the object store

nats::Esp32Updater::ConfirmRunningImage();                 // early in app_main
static nats::Esp32Updater up(*objectStore, *kv, "device-1");
up.StartAutoUpdate();   // watches KV "device.device-1.firmware.wanted"

Upload the .bin under the name given by sha256sum app.bin, then set the KV key to that name. The download is streamed into the OTA partition, and its SHA-256 is checked before the device reboots.

Linux / Raspberry Pi

sudo apt install build-essential cmake libmbedtls-dev
cmake -S nats-cpp -B build && cmake --build build -j

To use it from your own CMake project:

add_subdirectory(nats-cpp)
target_link_libraries(my_app PRIVATE nats-cpp)

Set the log level with -DNATS_LOG_LEVEL=0..4 (none, error, warn, info, debug). Level 4 also traces the raw protocol traffic, including credentials.

Tests

test/run_tests.sh            # everything
test/run_tests.sh kv         # only tests whose name or tag contains "kv"
test/run_tests.sh --list     # the test names read like a feature list
NATS_SANITIZE=thread test/run_tests.sh   # or: address (ASan + UBSan)

The script downloads the latest official nats-server and the nats CLI from GitHub for your architecture (amd64, arm64, or armv7/armv6 on a Pi) and caches them in test/.cache. Each test starts its own private server, with JetStream enabled, on a free port.

The suite covers:

  • unit tests for the protocol parser, JSON, NKeys, JWTs, base32/64, URLs and timestamps
  • regression tests, one per fixed bug
  • integration tests for core NATS, reconnect and keepalive (the tests stop, restart and freeze the server), authentication, TLS, JetStream, KV and the object store
  • compatibility tests against the Go nats CLI

To pin a server version, set NATS_SERVER_VERSION=v2.11.0. To use the binaries on your PATH instead, set NATS_USE_SYSTEM=1.

CI: .gitea/workflows/ci.yml (Gitea/Forgejo Actions) runs the Linux tests, both sanitizers, the ESP-IDF builds and, for pull requests, a clang-format check of the changed lines.

On the ESP32: test/esp_app is an ESP-IDF project that runs the same tests through the Unity menu.

  1. Set WiFi and the server URL with idf.py menuconfig, then run idf.py build flash monitor.
  2. Run the tests: * runs everything, [unit] only the tests that need no server.

The server must run with JetStream enabled: nats-server -js.

Platform differences

Feature ESP-IDF Linux
TCP / TLS yes (ESP-IDF certificate bundle) yes (system CA store)
WebSocket (ws / wss) with esp_websocket_client no
OTA updater (update/) yes no
Threads / randomness FreeRTOS / esp_fill_random std::thread / getrandom
Object store chunk size 16 KiB (default) 128 KiB (default)

All OS-specific code is in platform/. Dependencies: mbedTLS, which ships with ESP-IDF (on Linux: libmbedtls-dev), and Monocypher, which is included in third_party/.