Why module events share one stream, checked against the server

Storage is not a property of a subject — a stream is a separate object that
covers one — so the question is always how many streams, not which topics
are durable.

Three facts decide it, two of them verified rather than assumed: NATS
refuses overlapping streams instead of merging them, so a shared stream
plus a per-module one is not available at all; a filter cannot express an
exception; and a stream per module turns one cross-module consumer into one
per module. So one stream, with per-subject caps for the fairness that
matters. Per-module age is genuinely unavailable, and a module that needs
it declares a seat.
This commit is contained in:
2026-09-26 21:49:55 +02:00
parent 39c802cbd4
commit 0b8e84334f
@@ -103,6 +103,28 @@ seat: telegram-sender
If each consumer could tune it, the mesh's durability would be an emergent property of whichever If each consumer could tune it, the mesh's durability would be an emergent property of whichever
manifest was edited last. manifest was edited last.
**Why a module's own events do not carry their own retention, though the same rule would allow
it.** A seat owns its namespace and gets a stream of its own, so it can say. A module's events
share one `EVENTS` stream, and three facts about JetStream decide that they must:
- **Storage is not a property of a subject.** A subject is only an address; a *stream* is a
separate object that captures subjects matching a filter. So "this topic is durable" is always
really "some stream covers it", and something has to create that stream.
- **Overlapping streams are refused, not merged.** Verified against the server: a per-module
stream beside a shared `mesh.mod.*.event.>` is rejected with *subjects overlap with an existing
stream*. So "one stream by default, its own for a module that wants different retention" is not
available — it is all of one or all of the other, and a filter cannot express an exception
either.
- **A stream per module breaks cross-module consumption.** An audit logger consuming every
module's events is one consumer on one stream today; with a stream each it becomes one consumer
per module, created and destroyed as modules come and go.
So: one stream, and **per-subject caps** for the fairness that actually matters — a noisy emitter
cannot evict a quiet one, which is verified (a cap of three, ten messages on one subject and one
on another, leaves four). What is genuinely unavailable is a different *age* per module, because
JetStream ages per stream and not per subject. A module that truly needs its own retention has a
way to say so: declare a seat, which owns its namespace and gets its own stream.
**Scope gives per-node workers without a new concept.** A module running on three nodes that each **Scope gives per-node workers without a new concept.** A module running on three nodes that each
need their own queue declares a node-scoped seat: one holder per node, three queues, same need their own queue declares a node-scoped seat: one holder per node, three queues, same
machinery. Mesh-scoped and node-scoped seats already exist; here they do the work of "one shared machinery. Mesh-scoped and node-scoped seats already exist; here they do the work of "one shared