Skip to content
Nuvyntra Labs

Documentation

Technical reference

Service model, listeners, process configuration, streams, consumers, exchanges, on-disk layout, wire protocol, management HTTP API, and the limits of 0.1.1.

Overview

NuvexaMQ 0.1.1 is a single-node durable message broker. One append-only log stores each message. A stream, a queue, and an exchange binding are three ways to write or read that log.

The wire protocol is NuvexaMQ's own length-prefixed TCP protocol. It is not AMQP, MQTT, or Kafka. RabbitMQ is the model for virtual hosts, users, permissions, exchanges, queues, bindings, and policies. The bytes on the socket are different.

This version does not fail over to another machine. An acknowledged publish is on disk. Segment headers and commit.json store an epoch and a committed offset so a later follower can use the same files. The epoch on this node is 1.

Listeners

The process opens four listeners. A package install and the desktop app both use these defaults.

PortRoleAuthentication
5761Data. Clients publish, fetch, and ack here.Hello frame: optional cluster token, then user, password, and virtual host
5762Health and metrics. GET /health and GET /metricsNone
5763Management console and HTTP APICookie nuvexamq_mgmt or Authorization: Basic
5764Same console and API over HTTPSSame as 5763

GET /health returns {"status":"ok"}. GET /metrics returns Prometheus text.

The data port binds to 0.0.0.0 unless ListenAddress is changed. The banner prints tcp://127.0.0.1:5761 for a wildcard bind. With CertificatePath set, that banner is tls:// and the same frames are wrapped in TLS. Without a certificate, HTTPS on 5764 still starts with a generated certificate, and browsers warn until you replace it.

guest / guest is the seeded management user. That account is accepted only from localhost. A 401 body is JSON { "error", "reason" } and does not include a WWW-Authenticate header.

What you install

SystemPackageServiceData directory
WindowsNuvexaMQ-<version>-win-x64.msi or win-arm64.msiWindows service NuvexaMQC:\ProgramData\NuvexaMQ\data
macOSNuvexaMQ-<version>-osx-arm64.pkg or osx-x64.pkglaunchd com.nuventra.nuvexamq/Library/Application Support/NuvexaMQ/data
Linuxnuvexamq_<version>_amd64.deb or _arm64.deb, and the matching .rpmsystemd nuvexamq/var/lib/nuvexamq

The package installs the broker and the desktop app, puts nuvexamq on PATH, and starts the broker on boot. CI keeps the packages as workflow artifacts for 14 days. The tag v<Version> publishes them on the GitHub Release for that version. CI does not push them to NuGet.

The desktop window uses a different data directory: the user application-data folder NuvexaMQ/data. On macOS that is ~/Library/Application Support/NuvexaMQ/data. Starting the desktop while the system service already owns 5761–5764 fails with "Address already in use". The window then shows In use, and Stop unloads the system service (macOS asks for an administrator password because that service runs as root and restarts if it is only killed). Open admin portal opens http://127.0.0.1:5763/.

Process configuration

nuvexamq reads appsettings.json next to the binary, section NuvexaMQ, then applies flags.

FlagSettingDefault
--dataDataDir./data for a manual start. The package overrides this.
--portListenPort5761
--health-portHealthPort5762
--management-portManagementPort5763
--management-https-portManagementHttpsPort5764
--tokenTokenempty. An empty token accepts any hello token.
--cert / --cert-keyCertificatePath / KeyPathPEM certificate and key for the data port and for HTTPS
--flush-msFlushIntervalMs10
--management-user / --management-passwordseeded administrator, only when topology.json does not exist yetguest / guest
-v, --verboseconnection and publish linesoff

Other settings exist only in appsettings.json:

SettingDefaultMeaning
FlushMaxRecords256Group-commit flushes when this many records are waiting or when the flush interval elapses
MaxMessageBytes1048576Broker-wide maximum payload. A stream can set a lower limit.
MaxConnections10000Extra connections are closed
SegmentBytes67108864Rotate the live segment after this many bytes. The minimum accepted is 64.
ListenAddress, HealthAddress, ManagementAddress0.0.0.0localhost binds loopback

A publish acknowledgement means the record was inside an fsync. FlushIntervalMs 0 fsyncs every batch. The index file is not fsynced on the hot path. A crash can redeliver the last few milliseconds of acknowledgements because cursor writes are coalesced.

Verbose lines are a unix timestamp, then New connection from, New client connected, Received PUBLISH, and Client ... disconnected. Passwords and message bodies are not printed.

Command-line client, used by the same nuvexamq binary:

nuvexamq stream add --name orders --filter orders.>
nuvexamq pub --subject orders.created --body hello --key order-18
nuvexamq consume --stream orders --durable billing --count 1
nuvexamq path install

path install adds the published binary to PATH. It refuses a bin/Debug or bin/Release folder.

Data model

Streams, consumers, and exchange bindings are the objects stored on this node.

Streams

A stream is a named, partitioned log. EnsureStream creates it or, when the layout matches, updates retention.

FieldRule
Name1–128 characters: ASCII letters, digits, ., _, -, $
FiltersAt least one subject filter. The stream stores a publish whose subject matches any filter.
Partitions1–256. Changing the count later is a conflict.
Max ageMilliseconds, or none
Max bytesByte cap, or none. The live segment is never deleted by retention.
Max message bytes0 means the broker default

Subject filters are dot-separated. * is one token. > is the remainder and must be the last token, and the subject must have at least one token there. orders.> matches orders.created and orders.created.eu. orders.* matches orders.created only.

The partition is FNV-1a(key) % count. An empty key round-robins. The same key always lands on the same partition.

$dlq is the broker dead-letter stream for messages that exhaust MaxDeliver. It cannot be deleted.

Consumers

A consumer is a named cursor on one stream.

FieldDefaultMeaning
FilteremptyExtra subject filter on top of the stream. Empty accepts the stream's messages.
Ack wait30000 msMinimum 1 ms. An unacked delivery is eligible again after this wait.
Max deliver5After this many attempts the message is appended to $dlq
Max ack pending1000Cap on outstanding deliveries
EphemeralfalseAn ephemeral consumer starts at the tail and keeps no checkpoint. Release removes only ephemeral consumers. Releasing a durable name is a no-op. Releasing a missing name succeeds.
Startfirst0 first offset, 1 tail, 2 a specific offset

If the consumer name already exists, EnsureConsumer returns success and ignores the new start, filter, and ephemeral flag. Create the durable name once with the settings you want. Release an ephemeral name before creating it again.

A fetched message is not delivered again until nack, or until the ack wait expires. Nack of an unknown offset succeeds. Nack marks the delivery so the next fetch can return it with a higher delivery count.

Two durable consumers with different names on the same stream are fan-out: each receives the message. Two connections that use the same durable name compete: one of them receives each delivery.

If retention deletes a segment the cursor still points at, fetch sets the offset-reset flag and reports the first offset still on disk. Reset the consumer to continue.

Exchanges, queues, and bindings

Every virtual host has these built-in exchanges. They cannot be deleted.

NameType
empty name, shown as (AMQP default)direct. The routing key is the queue name. Do not bind it.
amq.directdirect. Routing key must equal the binding key.
amq.fanoutfanout. Every bound queue receives the message.
amq.topictopic. * is one word. # is the rest.
amq.headersheaders. Binding arguments are header name/value pairs. x-match is all (default) or any.

Declaring the same name and type again is a no-op. Declaring a different type is a conflict. Custom exchange types are direct, fanout, topic, and headers.

A queue is a stream whose filter is >. On virtual host / the log is named $queue. plus the queue name. On another virtual host it is $queue. plus the virtual host, a dot, and the queue name. Queue options:

OptionEffect
DurableRecorded on the queue. The log itself is always durable.
ExclusiveRemoved when the connection that declared it closes
Auto-deleteStored on the queue. The console does not offer this flag.
Message TTLMessages older than this are dropped from the visible range
Max lengthAfter a publish, older messages are dropped so depth stays within the limit
Dead-letter exchange and routing keyWhen TTL or the delivery limit expires, the message is published there. Header Nuvexa-Deaths counts hops and stops at 8.

A policy is a regular expression over queue names on one virtual host. The highest priority match wins. A later policy of equal priority replaces the earlier one. A null TTL, null max length, or empty dead-letter field on the policy keeps the queue's own value. The pattern match times out after 50 ms and then does not apply.

Permissions are three regular expressions per user and virtual host: configure, write, and read. .* allows every name. The resource name must match the pattern. A match that times out or a broken pattern denies the operation.

User tags administrator and management may call the management API. Other users can use the data port when their password and permissions allow it. Passwords are stored as a hash. The seeded user is created only when topology.json is missing, so later changes to --management-password do not reset an existing guest.

Resource names follow the same character rules as stream names. Virtual host / is special and cannot be deleted.

On disk

{dataDir}/topology.json
{dataDir}/streams/{stream}/stream.json
{dataDir}/streams/{stream}/p{n}/segment-{baseOffset}.log
{dataDir}/streams/{stream}/p{n}/segment-{baseOffset}.idx
{dataDir}/streams/{stream}/p{n}/consumer-{name}.json
{dataDir}/streams/{stream}/p{n}/commit.json

Log records are little-endian and carry a CRC-32 over the bytes after the checksum. On open, a torn tail is truncated. A missing index is rebuilt from the log. commit.json stores the epoch and the last fsynced offset.

Desktop logs, when logging is enabled, are hourly files {logsDir}/{yyyy-MM-dd}/log-HHmm.log. The desktop console keeps the latest 2000 lines.

Wire protocol

Every multi-byte integer is little-endian. A frame is:

FieldType
Lengthint32. Number of bytes that follow. Equals 6 plus the payload length.
Typeuint16 operation code
Request iduint32. The response copies it.
PayloadThe rest of the frame

The maximum frame is 32 MiB. A string is a uint16 byte count plus UTF-8 bytes. A blob is an int32 byte count plus bytes. An absent retention limit is int64 -1.

The first frame on a connection must be Hello. The server handles one request at a time on each connection.

CodeNamePayload
1Hellotoken, client name, user, password, virtual host. If the payload ends after the client name, user and password are empty and the virtual host is /.
2HelloOkversion string, 0.1.1
3EnsureStreamname, uint16 filter count, filters, uint16 partition count, int64 max age ms, int64 max bytes, int32 max message bytes
4EnsureStreamOkempty
5Publishsubject, key, uint16 header count, each header a string name plus a blob value, then the payload blob. An empty key round-robins.
6PublishOkuint16 receipt count, then stream, uint16 partition, int64 offset for each receipt
7EnsureConsumerstream, name, filter, int32 ack wait ms, int32 max deliver, int32 max ack pending, byte ephemeral, byte start, int64 start offset
8EnsureConsumerOkempty
9Fetchstream, consumer, uint16 max messages, int32 wait ms
10FetchOkbyte offset-reset, int64 first offset still on disk, uint16 message count, then each delivery
11Ackstream, consumer, uint16 partition, int64 offset
12AckOkempty
13Nacksame shape as Ack
14NackOkempty
15ResetConsumerstream, consumer, byte absolute, int64 offset
16ResetConsumerOkempty
17ReleaseConsumerstream, consumer
18ReleaseConsumerOkempty
19Pingempty
20Pongempty
21Erroruint16 code, message, int64 detail
22DeclareExchangename, type, byte durable, byte auto-delete
23DeclareExchangeOkempty
24DeclareQueuename, byte durable, byte exclusive, byte auto-delete, int64 message TTL ms, int32 max length, dead-letter exchange, dead-letter routing key
25DeclareQueueOkempty
26BindQueueexchange, queue, routing key, uint16 argument count. Each argument is two strings, not a blob.
27BindQueueOkempty
28DeleteQueuename
29DeleteQueueOkempty
30DeleteExchangename
31DeleteExchangeOkempty
32PurgeQueuename
33PurgeQueueOkempty
34PublishExchangeexchange, routing key, key, headers, payload. Same header encoding as Publish.
35PublishExchangeOksame receipt list as PublishOk

A delivery inside FetchOk is: uint16 partition, int64 offset, int64 timestamp unix ms, int32 delivery count, subject, key blob, uint16 header count, headers, payload blob.

Start bytes: 0 first, 1 tail, 2 offset.

Error codes:

CodeNameWhen
0InvalidMalformed name, bad filter, or an illegal operation
1UnauthorizedToken, password, or permission rejected
2NotFoundStream, queue, exchange, user, or binding target is missing
3ConflictPartition count, filters, or exchange type changed
4TooLargePayload exceeds the maximum
5OffsetResetThe cursor pointed at a deleted segment
6ClosedThe connection or broker is closing

Management HTTP

Public routes, with no login, are GET /, GET /icon.png, and POST /api/login. Everything else needs the cookie or Basic authentication. The cookie is HttpOnly, SameSite=Strict, path /, and lasts 8 hours.

POST /api/login body is { "username", "password" }. Success sets the cookie and returns { "username", "localhostOnly" }.

MethodPathBody
POST/api/logoutClears the cookie
GET/api/whoamiCurrent user
GET/api/overviewVersion, node, data directory, counters, listeners
GET/api/connectionsOpen data-port sessions
GET/api/channelsOne channel per connection. Prefetch is reported as 1000.
GET/api/streamsStreams that are not queues
GET/api/streams/{name}One stream
PUT/api/streams/{name}{ filters, partitionCount, maxAgeMs, maxBytes, maxMessageBytes }
DELETE/api/streams/{name}Deletes the stream. $dlq is refused.
GET/api/streams/{name}/messages?partition&count&offsetUp to 100 messages. Count defaults to 20.
DELETE/api/streams/{name}/messages?partition&offsetHides that offset
GET/api/consumers?streamConsumer cursors
POST/api/publish{ subject, key, payload } written to matching streams
GET/api/exchanges?vhost
PUT/api/exchanges/{name}{ vhost, type, durable, autoDelete }
DELETE/api/exchanges/{name}?vhost
GET/api/queues?vhost
PUT/api/queues/{name}{ vhost, durable, exclusive, autoDelete, messageTtlMs, maxLength, deadLetterExchange, deadLetterRoutingKey }
DELETE/api/queues/{name}?vhost
POST/api/queues/{name}/purge?vhostDrops currently visible messages
GET/api/bindings?vhost
POST/api/bindings{ vhost, exchange, queue, routingKey, arguments }
DELETE/api/bindings?vhost&exchange&queue&routingKey
POST/api/exchanges/publish{ vhost, exchange, routingKey, key, payload }
GET/api/usersNames and tags. Password hashes are not returned.
PUT/api/users/{name}{ password, tags }. A new user requires a password.
DELETE/api/users/{name}Also removes that user's permissions
GET/api/permissions
PUT/api/permissions{ user, vhost, configure, write, read }
DELETE/api/permissions?user&vhost
GET/api/vhosts
PUT/api/vhosts/{name}Creates the virtual host
DELETE/api/vhosts/{name}/ cannot be deleted
GET/api/policies?vhost
PUT/api/policies/{name}{ vhost, pattern, priority, messageTtlMs, maxLength, deadLetterExchange, deadLetterRoutingKey }
DELETE/api/policies/{name}?vhost

Failures use { "error", "reason" } with 400, 401, 404, 409, or 413.

Limits of this version

Clustering, federation, and the shovel plugin are not implemented. The data port is not AMQP. One request is handled at a time on each connection. The index is not fsynced on the publish path. Auto-delete is stored on the queue and is not applied when the last consumer leaves.

Discussion

Comment on NuvexaMQ. The thread lives on this component's GitHub repository (nuvyntralabs/NuvexaMQ). Sign in with GitHub — Giscus uses Discussions, Utterances uses Issues.