MERIDIAN

Multidomain Exchange for Realtime Integration of Data Innovation with Allied Nations

A low-friction, open-protocol, sovereign, decentralized exchange for real-time visibility and integration across humans, servers, agents, machines and IoT. That sentence is the product definition of record in AGENTS.md; every refusal later in this document is a refusal because it would cost one of those five words.

One binary that carries two very different kinds of traffic: signed, stored, indexed events for humans and agents, and opaque, ephemeral, header-routed frames for machines. It is a modular replacement for the bus, policy and audit role of a central MQTT broker — and a decentralization story that a broker cannot tell.

Who this is for. A Program Development Team joining cold. It is deliberately written so you can tell, on every line, the difference between what runs today, what is measured, what is designed but unbuilt, and what we have refused on purpose. Everything below is traceable to ARCHITECTURE.md, DIAGRAM.md, TASKS.md, AGENTS.md § Design Law, docs/mips/, .settings/gauntlet/register.md, .settings/features/, .settings/reference-docs/goat/, and the .beads issue database. Bead IDs are cited inline so you can run bd show <id> and read the source.

0 Two rules that govern every number on this page

Both are binding contract in AGENTS.md, not editorial preference. If you take nothing else from this page, take these — they are why the document is longer than a slide.

① No throughput figure without its profile

Traffic class, auth profile, whether the payload is parsed, and whether it is stored. One relay has two ceilings roughly two orders of magnitude apart. A bare number misleads in both directions, and quoting the good one without the qualifier is how you acquire an obligation the binary cannot meet.

② Nothing is resolved until deployed and re-probed

Green CI is not evidence that an enforcement path runs in production. A deployed image predating the enforcement code makes a feature flag a silent no-op; a trusted proxy header is forgeable unless something strips it at the edge.

And the register that stops those two rules from being decorative

Prose cannot forbid an agent — or an engineer — from declaring a component complete, secure or production-ready by self-assessment. A checker can. .settings/gauntlet/register.md carries 21 workstream rows (G01–G21), each in exactly one state — UNSTARTED → BASELINE → SLICE → REDTEAM → PASSED — with a gate class that decides what evidence counts: ci needs a named just … command, live needs a probe: against a real relay, bench needs a benchmark and its profile. just check-gauntlet fails the build on a row claiming a state its evidence does not support, and PASSED is unreachable without a recorded REDTEAM pass, because a self-graded gate is the exact failure the rules above name.

Read it before believing anything on this page. Today 14 of 21 rows are UNSTARTED. G02 and G17 are at BASELINE; G01, G03, G14 and G21 are at SLICE; G15 is at REDTEAM; and no row has reached PASSED. That is the honest maturity picture, and it is deliberately more conservative than the design maturity the rest of this document describes. The findings ledger now runs to F097 — 52 fixed, 45 open; each is a bead, because a markdown task list is forbidden by the root contract.

Three rows are worth reading in full, because they are what the loop actually buys you. G03 wired an isolated-DB gate that runs 157 previously #[ignore]d meridian-db tests in CI — and running them cost six findings, including the gate's green being a property of test order, proven by a drain→pass→4→6 sequence and by one module failing 5/5 alone while green in the full selection. It also retracted its own blocker: the "~2.5 h" cost estimate that had withheld the wiring re-measured at 119–224 s. G01 is parked at SLICE after 7 iterations — every regression repaired, remaining findings risk-class — which is the register admitting that a guard can absorb more effort than the thing it guards.

The sharpest result the loop has produced — and it is about evidence, not code

G03 went to REDTEAM and came back to SLICE. Three fresh critics were spawned with the artefacts and the repo's failure catalogue and no narrative from the author. They confirmed the engineering — the drain restructure is equivalent, no skipped or doubled goodbye, no hot spin; all 20 select! sites across both crates swept with no other live hazard; the cancel-safety mechanism real and in fact understated by its own comment.

Then they broke the ledger. The evidence cell asserted cargo test -p meridian-relay --lib exit 0. The tree exited 101 — one test defaulted to redis://127.0.0.1:6379, which on this host is a neighbour's container, not Meridian's. The claim was false in exactly the way this whole register exists to catch, and the row lost its state over it while the code it described was fine. That is the difference between a gate and a green checkmark.

measured a committed bench produced this number estimate derived, not measured — say so every time target a goal with a gate, not a result designed specified, no code in tree open a real question we have not answered refused deliberately not built, with a reason

1 The stack — three tiers, and what each may never do

Before the traffic-class argument makes sense, the shape has to. MERIDIAN is one self-hosted repository: source, builds and deployment all live in it, and there is no sibling pipeline to coordinate with. What runs is three tiers with one enforcement point between them. Source: DIAGRAM.md § Part I, ARCHITECTURE.md.

  CLIENT TIER — TWO EDGES, ONE RELAY

    NOSTR CLIENT EDGE  [in tree]                    MACHINE EDGE  [committed]

    Desktop     Mobile     Web        Agents        Feeders · IoT · server-to-server
    Tauri 2 +   Flutter +  repo       meridian-cli  peers. Speaks Zenoh natively —
    React 19    Riverpod   browser    ACP·dev-MCP   NOT NIP-01. MIP-XP link profile.
        │          │          │           │                        │
        └──────────┴────┬─────┴───────────┘                        │
                        │                                          │
     WSS · NIP-01 EVENT / REQ / CLOSE / AUTH        opaque frames · fixed header
     HTTPS · /events /query /count /media           never parsed · never stored
              /git /hooks /info                     MIP-OF envelope
                        │                                          │
                        └────────────────┬─────────────────────────┘
                                         │
  RELAY TIER — meridian-relay, ONE Axum process, the ONLY enforcement point

    STEP 0 · community bind      resolve_host → TenantContext
                                 runs BEFORE auth, event, req, REST, media, git
        │
    STEP 1 · admission           NIP-42 challenge / NIP-98 · connection semaphore
        │                        handler semaphore (1024)
        ├─────────────────────────────┬───────────────────────────────
        ▼                             ▼
    STEP 2 · EVENT pipeline       STEP 3 · REQ pipeline
    kind gate · BIP-340 verify    access check BEFORE registration
    drift · size · identity       historical query, then EOSE
    scope · membership
        └─────────────┬───────────────┘
                      ▼
    STEP 4 · SubscriptionRegistry — DashMap, 3-tier fan-out index

    SUBSYSTEMS, called only by the relay and never by each other:
    meridian-core · -db · -auth · -pubsub · -search · -audit · -workflow · -media
                                    │
                                    ▼
  STACK TIER — one system of record per fact

    Postgres 17            Dragonfly              MinIO / S3         Opt-in profiles
    THE EVENT LOG          STATE, never events    BYTES, never       keycloak · prometheus
    events · channels      pub/sub · presence     events             adminer · artifacts
    members · workflows    typing · rate windows  media blobs          │
    audit chain · FTS      caches                 git objects (CAS)    │
                                                       │              ▼
                                                       ▼         ReductStore
                                                  IPFS [planned]  [opt-in · no
                                                  CID replicas     call sites]
Two composition rules make one process reasonable at all. The relay orchestrates every subsystem by direct call, and the subsystems never call each other — meridian-workflow never calls meridian-pubsub; meridian-search never calls meridian-db. And both edges terminate on the same enforcement point and the same event log. A second edge is a traffic class, not a second authorizer; the moment it decides access on its own it has become one, which the no-third-shim law refuses. The workspace is 29 crates (plus examples/countdown-bot), asserted against the map in AGENTS.md in both directions by just check-architecture-map — every crate is a member, every member has a row, and the README's crate table is held to the same contract. Adding a crate touches three files and omitting any one fails CI rather than an audit.

Tier contracts — what each owns, and what it must never do

TierOwnsMust never
Client Rendering, local drafts, local-only settings, key custody, optimistic UI Hold authority. A client is never asked to enforce membership, and talks to nothing but the relay — no client connects to Postgres, Dragonfly, S3, or a consumer
Relay Community binding, admission, signature verification, ordering, membership enforcement, fan-out, re-authorization of every candidate Delegate an access decision. Being the only enforcement point is what makes the no-third-shim law free rather than expensive
Postgres The event log, and every fact derived by transaction — channels, membership, workflow runs, the audit chain Be second. Nothing else may claim truth for a fact it stores
Dragonfly Ephemeral state with a TTL — presence, typing, rate windows, replay set, caches — plus cross-pod delivery Become a durable log. It is a keyspace and a transport, never a system of record
Object store Bytes whose size makes them unfit for a row — media blobs, git packs Hold anything the relay must interpret to make a decision
Why this table is the load-bearing part of the diagram

Boxes and arrows are reproducible from any codebase; the must never column is the part that decays silently. Every architectural refusal later in this document — a second durable log, a second read path, a policy engine beside the relay — is this table applied. If you are proposing something and cannot say which row permits it, that is the finding.

What is actually in the repository

Relay + core

meridian-relay is the entry point and also hosts git and huddle audio. Around it: -core (types, verification, filter matching, the kind registry), -db (Postgres event store), -auth, -pubsub (Dragonfly fan-out, presence, typing), -search (Postgres FTS), -audit (hash-chain log), -media (Blossom/S3).

Agent surface

-acp bridges events to AI agents; -agent is a minimal ACP-compliant agent; -dev-mcp carries shell and file-edit tools; -persona, -workflow (YAML-as-code, evalexpr conditions), and -harness bundling the three.

Clients and tooling

REMAPPING/meridian-desktop/ (Tauri 2 + React 19), REMAPPING/meridian-mobile/ (Flutter), REMAPPING/meridian-web/ (repo browser), REMAPPING/meridian-admin-web/ (read-only operator dashboard), plus meridian-cli (agent-first), -sdk, -admin, git-sign-nostr and git-credential-nostr.

Deployment is in-repo too: deploy/charts/ (Helm) and deploy/compose/, images at registry.r2d2.office.ilab.zone/meridian-*, hosts under *.meridian.r2d2.office.ilab.zone. A separate control plane provisions per-account relays and is deliberately not a second source of truth for tenancy — it stores the community_id → host bridge and executes every mutation against the relay's own NIP-98 operator API.

Two properties of this stack that surprise people

Dragonfly, not Redis. It speaks the Redis wire protocol, so REDIS_URL, the redis crate, Lua scripts and pub/sub all work unchanged — but the container is meridian-dragonfly and reintroducing a redis service is a regression, not a rollback. The artifact tier holds no truth. ReductStore is opt-in, serves bytes on the consumer axis, and currently has no call sites — wire a consumer or remove it, exactly as a self-hosted Convex tier was removed rather than left as inventory.


2 The one mental model: two relays in one binary

Almost every wrong intuition about this system comes from collapsing these two paths into one. They share a process and almost nothing else. Making that seam a deliberate module boundary — rather than an emergent property discovered when someone adds a parse() to the hot loop — is tracked as meridian-tg1. ARCHITECTURE.md states the same fact from the ingress side — "one binary, two data planes", reached through the two edges in §1 — and the two framings are the same seam seen from opposite ends.

  HUMAN + AGENT PATH  ── NIP-01 over WebSocket ─────────────────────────────────
    parse JSON → BIP-340 verify → Postgres INSERT → FTS index → audit chain
                 └─ 34–37.7 µs ─┘   └───── the durability boundary ─────┘
    ceiling: ~250–320k events/s per 16-core pod (verify-bound)
             ~10–30k/s once stored AND indexed

  MACHINE PATH  ── opaque frames, ephemeral kinds, trusted link ───────────────
    read ~44 B fixed header → match key expression → forward bytes, never parsed
                 └─ no signature, no store, no index, no audit ─┘
    ceiling: ~600k msg/s ≈ 0.44 Gbps in   (ESTIMATE — path not yet in tree)
    target:  1.0–1.5M msg/s per pod after the batching refactor  (TARGET, gated)

  SHARED:  the community/tenant fence · admission · interest routing · the bus
  NOT SHARED:  parsing · crypto · durability · query surface · client shape
The machine path's defining property is a refusal: the relay never reads the payload. That is not a limitation to work around — it is the enabling property for zero-copy fan-out and for the throughput being asked for. Everything the relay must act on therefore has to live in the header.
The governing rule of the machine path

Anything the relay must act on MUST live in the header; everything else is bytes it forwards and never reads. That single sentence allocates the whole design: dedup moves upstream to the feeder (it requires comparing content), conflation stays at the relay (its key is a header field), NIP-01 tag filters are ruled off the hot path, and zero-copy fan-out becomes possible.

What the header carries, and why each field is there

FieldWidthWhy the relay needs it
community16 Bthe existing tenant fence — bound before any handler sees data
kind4 Bselects the QoS lane and the class floor. This is also payload_type — MIP-AS returned that resolution to MIP-OF, because the relay's only two uses of either are the lane and consumer discrimination, and the 44-byte budget has room for one field, not two
geo cell4 Bgeohash-3/4 packed; this is the routing key
conflation key8 Btarget id — ICAO24 (24 b) and MMSI (30 b) both fit. Makes anti-shredding implementable
sequence4 Bgap detection without reading content
timestamp8 BTTL / staleness without reading content

≈ 44 B fixed-width, no allocation, no parse tree. Spec: MIP-OF (meridian-tdq) — DRAFT. The encoder is postcard, already a workspace dependency. A worked budget exists: MIP-CT's ≤37 B core track payload on this header is ≤81 B fixed-width, ~75 B after varints — for a track carried without source attachment, which is exactly the kind of profile this page refuses to drop.

Consequence to accept deliberately

Opaque machine frames are not consumable by a generic Nostr client. Consumers are purpose-built — a track display, a fusion engine, a recorder. That is the intended shape (general-use relay, specialty MIPs), and it belongs in the spec rather than being discovered in integration.


3 Replacing TBMQ — what is drop-in, what needs an adapter

The honest version of the claim, and the one the Architecture & Scale council approved: Meridian replaces the broker's bus, policy and audit role. A broker remains legitimate — often correct — at the constrained-device edge, feeding the spine through a feeder and never as a routing peer. Read the three tables below in order; the third is the one that keeps us honest.

Drop-in today — the same job, done differently

MQTT / TBMQMeridian equivalentStatus
PUBLISH to a topicsigned event, or an opaque frame on a key expressionruns
SUBSCRIBE with wildcardsNIP-01 REQ filters (human path) · key-expression subscription (machine path)runs / designed
Topic ACL evaluated per messagebound once at connect and at subscribe, from a host-derived tenant fenceruns
Kafka retention behind the brokerthe events table is the log, the query store and the FTS indexruns
Broker-vouched publisher identityper-event BIP-340 signature that survives the relay, storage, and third-party re-verification years laterruns
Cluster bridging between broker nodesinterest-scoped cross-pod fan-out over one bus, no N² bridge configruns — 64× ingress reduction measured, at the subscribe side; the publish side is unconditional today
QoS 0 fire-and-forget telemetryephemeral kind range 20000–29999 — skips storage, audit and search by compile-time fenceruns

Needs an adapter — real work, sized and owned

What a broker gives youWhat we do insteadCost / owner
Broker-held offline queues, persistent sessions (QoS 1/2, non-clean session) Clients reconnect and re-run a REQ over stored events. Works for a chat client; does not work for a constrained device that cannot page history. will not add a second durable queue — keep a broker at the edge. Now partly specified: MIP-DD (meridian-bm32) defines DDIL session resume — a client on an intermittent link resumes a session that never logically ended, rather than reconnecting. It is not a queue and does not become one
Device SDK ecosystem Every embedded platform ships an MQTT client; almost none ships a Nostr client. specified MIP-AD, the adapter contract (meridian-qi7f) — this is the reusable asset (§6). Profiles built on it: MIP-ML MAVLink, MIP-NM NMEA 0183, MIP-RI Remote ID, MIP-DS ROS 2/DDS, MIP-AS AIS
Shared subscriptions $share/group/topic Every matching subscription receives every event; competing consumers must coordinate themselves. still no equivalent — MIP-MQ is where it would land, and it does not carry one yet. Name it before a workload depends on it
Retained messages, Last Will & Testament Presence with a 90-second TTL does part of LWT's job. NIP-33 addressable events resemble retained messages without being them. partial — gap is real, not papered over. The machine-path answer is a health beacon, not a broker primitive (§7)
QoS 2 exactly-once At-most-once on the bus, idempotent at the store (ON CONFLICT DO NOTHING) → at-least-once-with-dedup end to end. different contract — but no longer an undescribed one. MIP-CU (meridian-8qkw) separates the five things NIP-01 OK conflates: accepted, stored, authorized, delivered, acted on. A caller can finally tell which it received

Why the broker shape stops working as we scale

Eight arguments, all decidable from shape rather than from a benchmark:

ArgumentMechanism
1 · The hop is mandatory; ours is architecturally conditional — but not yet conditional in codeEvery TBMQ message crosses Kafka even when publisher and subscriber sit on the same node. We serve local subscribers from an in-process DashMap, so the delivery path for a local subscriber never touches the bus. Correction, and it is ours to own: this page previously said we "touch the bus only for cross-pod reach, only on retained topics." Redis PUBLISH is unconditional today — neither condition is built, and no topic-retention concept exists in the codebase (DIAGRAM.md:215). The shape argument survives — broker cost grows with total traffic, ours can grow with cross-pod traffic, and tenant sharding reduces that term — but it is a property of the design, not a property of the running binary, and the 64× measurement is of interest scoping at the subscribe side.
2 · Kafka is a second durable logWe already have one. A second buys retention we have and costs an operational tier plus truth ambiguity. This is refusal R2, written before TBMQ entered the conversation.
3 · Authorization binds per messageMQTT topic ACLs are evaluated on every PUBLISH and SUBSCRIBE. We bind at connect and subscribe. Stated honestly: we are better placed, not clean — filter_fanout_by_access is our own per-recipient residual.
4 · Message authenticity dies at the brokerAn MQTT message is authenticated by its connection. Downstream there is no proof of authorship, so audit chains, third-party verification and cross-operator federation must be rebuilt above the transport. This is what the ~37.7 µs buys.
5 · QoS is a delivery dial, not a congestion dialQoS says how hard to try to deliver. It says nothing about which class to shed under load. A 100 Hz telemetry stream and a chat message should not share a movement contract. Caveat: our eight-class map and conflated are designed, not built — today we too have one undifferentiated queue.
6 · Tenancy is a topic-prefix conventionMQTT has no tenant boundary in the protocol, so multi-tenancy becomes prefixes plus ACLs — exactly the "names encode ownership" failure our Design Law refuses, where a reorg becomes a fleet-wide ACL edit. Our community is host-derived, immutable on the row, and part of every primary key.
7 · One scaling axis, not fiveTBMQ scales by adding broker nodes and Kafka partitions. It offers nothing for function-split, tenant-shard, traffic-class or trust-domain federation. Those four are what §4 is built on.
8 · Bridging is O(N²)MQTT bridges are point-to-point configuration; N interconnected networks need N² hand-maintained bridges. Interest-based transitive routing replaces that at O(N) — and is blocked here on cryptographic enforcement work, which we say rather than imply.
The second thing to own before making argument 2 in a meeting

Refusing a second durable log is right, and the reason is that the events table is already the log. It is not yet right for the reason a reader might infer from a durability diagram. DIAGRAM.md now opens its write-path section with TARGET STATE, NOT AS-BUILT: ingest_seq, the commit watermark and the consumer cursor do not exist — grep -rn ingest_seq over migrations/ and crates/**/*.rs returns zero.

What exists today is a client-driven REQ backfill capped at 2000 (handlers/req.rs:25) with no gap detection. So "consumer down? no message is lost" is a property this design would buy, not one the system has. Do not cite the durable cursor as the reason no broker is needed until the column exists (meridian-5riu). Cite the events table, which is real.

  THE RATIFIED TOPOLOGY — hub and leaf, never peer

   constrained devices       EDGE — MQTT bridge          FEEDER              SPINE
   MQTT clients,        ──►  + LEAF RELAY          ──►   decode        ──►   ONE authoritative
   intermittent links        persistent sessions         dedup               relay per community
   need offline queues       QoS 1/2 · retained          routing header      admission
                             LWT · shared subs           MAC or sign         verification
                             store-and-forward           batch               tenancy fence
                             across outages                                  audit · fan-out

   ✕ REFUSED: the leaf as a routing peer.
     A leaf outage would become a spine routing failure, and a compromised
     leaf could transit between communities.   (Refusal R4 · Law 2)
The boundary is one-directional: the spine ingests from the edge; the edge never routes for the spine. The feeder is where the expensive, content-aware work belongs — because a relay that never reads the payload categorically cannot deduplicate by content, and that refusal to read is the enabling property, not a limitation.

The load-bearing consequence: single-writer is preserved. Leaves forward upstream to exactly one authoritative relay per community, so the epoch-monotonic revocation defect (meridian-3wh, §4) stays dormant rather than becoming live. A topology that made leaves into peers would activate it — which is why this is a ratified constraint and not a deployment preference.
Say this precisely, every time

There is no head-to-head benchmark. Nothing here is a measurement of TBMQ, and nothing here is a measurement of Meridian against TBMQ on one workload. The architectural argument is decidable from shape; the numeric comparison is not. Publish this as an architecture and cost-model comparison, never as a benchmark result.


4 The decentralized story — five axes, and eight refusals

A broker scales one way: more broker nodes, more partitions. We scale five ways, and the discipline that makes that safe is the refusal register — the moves this architecture will not make, each with its reason and its review-time tell.

The one-line rule

Shard by who owns the data and by what job runs — never by what the data is called. A kind is a column in every shard, never a shard.

AxisYou addFailure storyStatus
A1 · Replica — identical spine pods behind one URLa podlose a pod → nothing happenstoday
A2 · Function — truth on the spine, derived views in consumersa consumer containerlose a view → staleness, publishedproposed
A3 · Tenant — shard whole communitiesa community shardlose one tenant → the others keep runningkey already immutable
A4 · Traffic class — persistent vs ephemeral, QoS lanesa lane, an edge podlose droppable traffic → by designrange fenced; lanes unbuilt
A5 · Trust domain — networks federate rather than mergea peered networklose a peer → the local network stays wholeblocked — see below

If a proposed split's failure story is not one of those five sentences, it is on the wrong axis.

A5 is blocked on one specific defect — know it before you propose federation

NIP-33 addressable events resolve last-write-wins by created_at. Membership already lives in that range (39001/39002). So a revoke and a concurrent grant healing across a partition, or across two relays, resolve by later timestamp and resurrect the revoked grant — silently.

The rule is banked in Design Law: revocation is monotonic and absorbing, carried by an explicit epoch/generation counter over an append-only log, resolved fail-closed. Once revoked at epoch n, no grant at epoch ≤ n reinstates. Decided now, built at the second node (meridian-3wh) — dormant while one relay is the only writer, and it must land before any relay-to-relay federation, because after is too late.

The refusals — bring these to design review

RefusalWhy it fails hereThe tell in review
R1 shard by kind/NIPreads are cross-kind by construction; gift wrap encrypts the inner kind so the router cannot see its own key; kind volume is skewed → one hot shard, N idlea deployment map or ACL keyed by kind
R2 a second durable logthe events table is already the log; a second buys retention we have and costs truth ambiguitya broker with a retention setting
R3 a second read path for data the spine stores"a correctness liability for zero measured gain"a consumer answering filters Postgres already serves
R4 consumers promoted to routing peersa consumer outage becomes a spine routing failure; a compromised consumer can transit between communitiesa consumer in router peering config
R5 grant copies in the scaled-out partthe third shim — a second policy store drifts from the firstan ACL cache with no freshness stamp; a sync job between two stores
R6 client-side multiplexing as the scale storyworks in open Nostr precisely because there is no membership, tenancy, ordering or audit contract — that contract is this producta client holding a per-capability relay list
R7 parallelizing what serializes by meaningthe audit chain and per-community ordering are serial because their semantics are serial; the sanctioned move is A3"shard the chain" / "split the arbiter"
R8 scale-out ahead of a measurementmachinery for load that does not exist is inventorya new tier whose PR carries no Phase 0 number

Four invariants every axis must preserve

  1. The edge cannot tell — NIP-01 unchanged; no client learns the internal shape.
  2. One system of record per fact — everything else is a cursor-bearing, freshness-stamped projection.
  3. Stale, never wrong — a scaled-out part may lag, with the lag published; it may never lose or reject.
  4. No new authorizer — scale-out never adds a place where policy is decided.

5 Throughput goals — every figure with its profile

This is the table to memorize. Note how many rows say estimate. That is the point: the discipline is what makes the measured rows worth quoting.

ProfileFigureBasis
Opaque pre-decoded frames · trusted P0 link · payload never parsed · not stored ~600k msg/s
≈ 0.44 Gbps
estimate The path does not exist in tree. The pre-/post-dedup question behind it is open at ~10×.
Same profile, after the batching refactor — the program's stated goal 1.0–1.5M
msg/s per pod
target MRDN-810 is the gate. The scoping figure behind it is 1.337M msg/s, and that number is TBMQ-class vendor-published, not measured on our stack — MRDN-101 establishes our own baseline before anything commits to it.
Signed NIP-01 events · per 16-core pod · BIP-340 bound ~250–320k
events/s
measured per-core, extrapolated verify_event = ~30k/s on one core, observed 21.6–33.1k over 3 runs (M3 Max, release, one core, batch 1/8/64 — just bench). At ~30k/core a 16-core pod tops out near ~480k/s for verification alone, 350–530k across the observed spread.
Stored and indexed chat events · one Postgres ~10–30k/s estimate Persistence latency is measured — p95 74.6 ms direct, 77.1 ms nested, synchronous_commit, concurrency 1.
Per-event BIP-340 verify (single; no batch API exists) 34–37.7 µs measured meridian-core/benches/event_cost.rs. Flat across batch 1→64.
Fan-out, per event ~4.1 µs @ N=1
~56 µs @ N=64
measured fanout_cost.rs — a fixed ~2–3 µs JSON serialize plus ~430 ns per recipient. Past ~40 recipients, fan-out exceeds signature verification.
Symmetric MAC alternative to per-event signatures
compare at a matched batch size
~120× / ~400×
at batch 1
~200× / ~420×
at batch 64
measured HMAC-SHA256 / keyed BLAKE3. Honest bands at batch 1 are ~110–170× and ~360–550×, because they divide by the BIP-340 number whose observed spread is 21.6–33.1k/s. schnorr_verify_nostr is flat across the 1/8/64 sweep; the MAC groups are not, so a MAC figure without its batch size is a figure without its profile.
Bus — Dragonfly today, interest scoping 64× ingress
reduction
measured perf/relay_bus_scaling.py --mode redis, 64 communities, 1 subscribed, pods 1/2/4.
Bus — Zenoh, peer mode, one pod pair, 256 B payload ≥2M msg/s target, unproven Gated on Phase 0 (meridian-ctg). If Zenoh is under 5× on this named profile, we stop and defer the whole chain.
Two rows above were re-stated at their real precision — read this as the method working

The verification ceiling used to read 26.5–29.4k events/s. Re-running the same bench on the same machine reproduces 21.6–33.1k, so the published interval was narrower than the measurement supports — and nothing isolated the bench from host load. The number was never wrong; its precision was. Every derived figure moved with it: the pod ceiling from ~420–470k to ~480k with a 350–530k band, and the MAC ratios from exactly 125×/405× to ~120×/~400× with bands, because they divide by the number whose spread this is (meridian-9fqe).

That is rule ① applied to ourselves rather than to a vendor, and it is the reason just bench now exists as a recipe: it prints the CPU, core count, OS, traffic class, auth profile and parsed/stored status alongside the numbers, so a figure cannot be lifted out of its output bare. Seven bench groups run — every group backing a published figure on this page.

The same species of defect was found twice more while reconciling this page, and both are now fixed at the source. (1) AGENTS.md stated the MAC advantage twice — ~120–400× in one paragraph and 179–382× two paragraphs later, as though they were the same quantity. (2) ARCHITECTURE.md quoted HMAC at 3.67 M/s while TASKS.md quoted 5.27 M/s / 0.19 µs, and neither said which batch size it meant.

One root cause, and it is instructive. The bench sweeps batch 1 / 8 / 64 and records both endpoints. schnorr_verify_nostr is flat across that sweep — which is the whole point of sweeping it, since with no batch API cost is strictly linear in N — but the MAC groups are not flat: HMAC-SHA256 runs 3.67 M/s at batch 1 and 5.27 M/s at batch 64. ARCHITECTURE was quoting batch 1, TASKS batch 64; both were right and neither said so. And 179–382× turned out to be neither — it divided MAC throughput at batch 64 by Schnorr throughput at batch 1, a mixed profile matching no row of the bench's own table. It had propagated from the bench's doc comment into four documents.

Matched-profile ratios, which is what this page now carries: batch 1 → 125× and 405×; batch 64 → 199× and 423×. A ratio carries its batch size for exactly the reason a throughput carries its traffic class — which is rule ① again, one level down. Neither error changed a conclusion: BLAKE3 wins on every reading, by two orders of magnitude.

What the target costs on each path — the argument in one table

Ingest CPU required to sustain 1,337,000 msg/s on one host, profile ingress, QoS 0 equivalent, small payload, never stored:

PathPer-msgCoresVerdict
Signed NIP-01 events37.7 µs m50.4Not viable — ~3.2 × 16-core pods for verification alone, before routing, framing or egress
Opaque frame + HMAC-SHA256 @ batch 640.19 µs m0.25viable
Opaque frame + keyed BLAKE3 @ batch 640.089 µs m0.12viable — specify BLAKE3. It is currently a meridian-core dev-dependency and must be promoted to a production one (MRDN-312)
Opaque frame, P0 trusted link, no MAC00viable where the link is trust-domain-internal
Pipeline overhead, unbatched~2 µs e2.67the dominant cost today
Pipeline overhead, batched at 64~150 ns t0.20the meridian-0zd win

The target is architecturally reachable on the opaque-frame path and architecturally unreachable on the signed path. That is the whole case, and it must always be published with both halves — a figure quoted without the second row is exactly the obligation rule ① exists to prevent.

The arithmetic that changes the intuition

At 10M users / 1M concurrent, human message volume is only ~3.3k events/s; with agents at 5× human volume, ~20k/s. One verification pod covers it. Fan-out at an average 50 recipients is ~1M frames/s.

At vast scale this system is fan-out bound, not crypto bound — the opposite of the per-pod ceiling story, and the number that should drive any comparison. Large-channel fan-out is named as the honest open problem: it is not solved, and fan-out-on-read or a broadcast class is the unbuilt answer.

Where the machine path's constraint actually sits: egress, not ingest

    egress  =  R_in  ×  Σ  f_s        f_s = the fraction of the keyspace
                        s              subscriber s declared interest in

    Ingest is a fixed ~0.44 Gbps at 600k msg/s.   EGRESS MULTIPLIES.

    10 subscribers, all want everything (f=1)  →  6M msg/s  ≈ 4.4 Gbps
    10 subscribers, 1% of cells each           →  60k msg/s ≈  44 Mbps
   100 subscribers, 1% of cells each           → 600k msg/s ≈ 0.44 Gbps
Interest scoping is the only lever on the controlling term. That is the metric that should judge every item on the roadmap: does it improve interest scoping? Batching, conflation, geo-keying and federation ACLs all reduce it. Governance features do not.

Ingest cost model at 600k msg/s, transmission only

CostAt 600k/sNote
Signature (BIP-340)0ephemeral + session frames; not on this path
Link auth (P0 profile)0mTLS/QUIC peer identity on a trusted feeder link
MAC (P1 profile, if required)~9% of a core~150 ns each
Header parse + key matchnegligiblefixed-width, no parse tree
Async pipeline overhead~0.6–1.8 coresthe dominant ingest cost — ~1–3 µs/msg estimate
Bandwidth~55 MB/s600k × (32 B header + ~60 B state)

Ingest is cheap; the relay can carry the firehose directly — if and only if it never parses it. Aggregation is an optimisation, not a precondition. The single structural fix for the dominant row is meridian-0zd: make the batch the type flowing through the pipeline (mpsc::Sender<FrameBatch>, not <Frame>), with a synchronous inner loop. Estimated 10–20×.


6 Transport and the adapter contract — latency, lanes, buffers

Two pieces of work sit between a device feed and the spine, and they are the highest-leverage reusable assets in the program.

Correct the most common misreading of the transport plan first

The Zenoh peer runs inside the relay process. That is the whole point, and zenohd is not the destination. Today every cross-pod event takes two hops (pod A → Dragonfly → pod B). In peer mode pods connect directly and the broker hop disappears — a latency win that is independent of any throughput figure. The zenohd router daemon is added only where a full peer mesh stops being viable, because peer links grow with the square of the pod count; a small or single-region deployment may never run one at all. When it is added it goes under a new opt-in bus compose profile, default off, because core services must stay profile-free — just _ensure-services blocks on core health and would hang 120s against a disabled service.

And the seam matters more than the vendor. The sequence is an EventBus trait seam first (meridian-8rn), then a ZenohEventBus in peer mode behind an env gate with dual-publish shadow mode (meridian-bem), then QoS classes, then routers (meridian-0ps). Nothing below that seam exists in the workspace today — there is no zenoh dependency in any Cargo.toml, and Zenoh is not authorized by the program plan until meridian-ctg clears.

Zenoh as the spine — surface by surface

Zenoh is the transport spine for everything except the Nostr client edge. That exception is not a compromise; it is what lets the spine exist at all, and it keeps Nostr interoperability — the project's stated core value — intact.

SurfaceZenoh's roleAxisGate before it lands
Inter-pod busreplaces Dragonfly PUBLISH/PSUBSCRIBE, peer mode in-processA1ctg → 8rn → bem
Machine / IoT / agent ingressnative Zenoh edge, MIP-XP profile — not NIP-01A4MIP-XP + MIP-SF (owl)
Relay ↔ relay meshcandidate consolidation with the existing iroh QUIC meshA1must be decided, not deferred
Cross-org federationzenohd routers peering across trust domainsA5blocked on epoch-monotonic revocation
Nostr client edgenone — NIP-01 JSON over WebSocket stays—permanently out of scope

Three properties keep this checkable in review: Zenoh adds no event kind and no client-facing NIP (MIP-SF adds a message type, SFRAME, in the existing ephemeral range); Zenoh is never a store — zenoh-backend-* storages exist and are not a transactional store with FTS, partitioning and an audit chain; Dragonfly is narrowed, not displaced — presence TTLs, rate-limit INCR+EXPIRE, the NIP-98 replay set and the fenced arbiter all need a keyspace with atomics, and Zenoh is a pub/sub fabric, not a keyspace.

The adapter / feeder contract — build once, second feed is configuration

MIP-AD (meridian-qi7f) is now a written spec, and it is the mechanism that makes every subsequent protocol profile thin. It pins the four things every feed argues about and no envelope can infer: units, coordinate reference system, time base, and declared lossiness. Profiles built on it — MAVLink, NMEA 0183, Remote ID, ROS 2/DDS, AIS — add no mechanism of their own. Six obligations:

1 · Decode upstream

The relay never parses. All content-aware work happens here, before the spine.

2 · Dedup here

Dedup compares content; the relay has forsworn content. Shard it geographically so the dedup key and the routing key are the same key — no shared state.

3 · Compute the routing header

Geohash cell, conflation key, sequence, timestamp. The feeder already decoded the position, so this is free here and impossible downstream.

4 · Sign or MAC

Per the destiny rule, not per a performance preference — see the profile ladder below.

5 · Batch

64 states per frame turns ~600k pipeline traversals/s into ~9.4k, and header overhead from ~35% to ~6% (~25% bandwidth saving).

6 · Attest, don't impersonate

The author is the feeder: "I, receiver R, observed these states at time T." Enforced by NIP-70 protected events.

The exchange-profile selection rule (MIP-XP, meridian-2e2)

The profile floor is set by the message's destiny, not by a performance preference. You cannot negotiate away the signature on data that leaves the trust domain, because the signature is what makes it leave-able. Negotiated once per link; enforced per message.

P0 none — link identity only (mTLS/QUIC), 0 cost, 0 bytes · P1 MAC ~50–200 ns · P2 ed25519 batch-verified ~5–8 µs · P3 BIP-340 ~30–70 µs, NIP-01 native.
Per-class floors: ephemeral = P0 · persistent = P2 · stored-federated = P3.

QoS lanes — the buffers each class needs

Today all of these share one undifferentiated lane, so a burst of typing indicators can consume buffer that chat needs. The map below is designed (Zenoh Phase 3); the bottom two rows are the proposed drone/machine additions.

ClassPriorityCongestionReliabilityexpress
Fence / control — ban enforcement, cache invalidationRealTimeBlockReliableyes
Huddle audio — Opus framesRealTimeDropBestEffortyes
Huddle control · moderation 9000–9044InteractiveHighBlockReliable—
Agent observer (24200)InteractiveLowDropBestEffort—
Jobs / workflowsDataHighBlockReliable—
Chat · reactions · deletesDataBlockReliable—
Typing · presence (20001/20002)DataLowDropBestEffort—
Push leases · backfill · reindexBackgroundBlockReliable—
Kinematic state frames (proposed 21000)RealTimeDrop → conflateBestEffortyes
Sensor / health telemetry (proposed 21001/21002)InteractiveLowDropBestEffort—
The rule that keeps the lane map honest

Do not merge a kind constant without a QoS class. kind_to_qos() lives next to the constants in meridian-core/src/kind.rs, is exhaustive over the documented ranges, and carries a compile-time assertion per range. It is the twin of the existing rule that no kind may merge without a reader.

Dynamic buffering — the three knobs that matter

KnobWhat it buys
transport/link/tx/queue/size/{control,real_time,interactive_high,…}Deep queues for Data, shallow for RealTime — so audio and kinematics drop rather than delay. A deep queue on a realtime lane is latency debt disguised as reliability.
transport/link/tx/queue/congestion_control/{drop,block}/wait_before_closeThe exact knob for "shed typing indicators under load instead of buffering them."
transport/link/tx/batch_size (default 65535) + adaptive batchingCoalesces small messages into one network batch when the link is busy and sends immediately when idle. This is where the msg/s figures come from, and it is precisely what one-PUBLISH-per-event cannot do.
The Zenoh trap — name it in every review of this code

Zenoh already batches at the transport layer. That win is completely destroyed by for msg in batch { tx.send(msg).await } immediately above it. The batch must survive all the way to fan-out, or the transport batching buys nothing. It also amortises TLS record framing for free — one record per batch rather than per message, which matters because per-record overhead at 600k records/s is significant while bulk AEAD at 55 MB/s is not.

Topology — and where zenohd optionally enters

  TODAY          Pod A ──┐                        two hops per cross-pod event
  [in tree]      Pod B ──┼──► Dragonfly ──►       SPOF + broker hop
                 Pod C ──┘

  PHASE 2        Pod A ◄──► Pod B                 peer mode, ONE hop.
  [committed]       ▲  ╲   ╱  ▲                   THE PEER IS IN-PROCESS in
   meridian-bem     │    ╳    │                   every relay pod — this is
                    └─► Pod C ┘                   the destination, not a
                                                  waypoint.

                                                  Dragonfly stays — STATE ONLY:
                                                  presence TTL, rate limits,
                                                  replay set, fence generation

  PHASE 4        region 1: Pod A ─┐                    ┌─ Pod C :region 2
  [OPTIONAL]                      ├─ zenohd ◄─► zenohd ─┤
   meridian-0ps  region 1: Pod B ─┘                    └─ Pod D :region 2

                 Added ONLY where a full peer mesh stops being viable
                 (peer links grow with the square of the pod count).
                 Opt-in `bus` compose profile, default off.
Note what survives in every panel: Dragonfly never leaves. It stops carrying events and keeps carrying state. It is a keyspace and a transport, never a system of record. And note what is conditional: a single-region deployment can take the entire Phase 2 latency win and never run a router.
The security item that must not be gotten wrong

Today the relay does not re-verify signatures on events arriving over the bus, justified by a documented trust assumption: the bus only ever carries events between nodes inside the relay trust domain. That holds because Dragonfly is a private, in-cluster service.

zenohd routers make cross-org federation cheap, and cheap federation is how that assumption dies quietly. The moment a router peers with a relay outside the trust domain, "trusted bus" becomes false without any code changing. This must be fenced explicitly before Phase 4, not after.


7 Anti-shredding — time-based, not transactional

This is the trade the brief names, and it deserves its own vocabulary because it is the single most likely thing for a team arriving from MQTT to get backwards.

MQTT is transactional

QoS 0/1/2 is a promise about a message: deliver it at most once, at least once, exactly once. Under congestion the broker's options are to queue it or lose it. Neither option knows that message #941 for target X has been made meaningless by message #942.

We are time-based

A kinematic frame is a superseding snapshot: the newest state for a target replaces the previous one, and the older has no residual value. The correct behaviour under congestion is therefore not to queue and not to shed randomly — it is to keep the latest state per target and discard superseded entries.

Why "anti-shredding" is the right name

For superseding state, a conflating buffer under congestion loses nothing. Every target is still delivered at its newest position; only the update rate degrades. No target ever disappears from the picture. A dropping buffer shreds the picture — targets blink out at random. A conflating buffer ages it — every target is present, possibly a second stale.

Three movement contracts — and why this is protocol, not a buffer policy

ContractUnder congestionWhat the subscriber is promisedStatus
persistednever droppedcomplete, ordered, storedruns
volatiledroppedgaps possible — samples genuinely lost; a target may vanish from your displayrange fenced, contract undeclared
conflatedsuperseded entries dropped, by keyno target lost, rate degrades; every target present, possibly a second staleDRAFT MIP-QC · meridian-9j8
Why the stream must declare its contract

An operator watching a tracking picture has to know which guarantee they have. An undeclared stream forces the consumer to assume the weaker one — which throws away the entire benefit. So the contract and its conflation key are part of the wire spec, not an implementation note. And it is only implementable at all because the conflation key lives in the header.

The second, larger payoff: conflation subsumes receiver dedup

Applied at ingest rather than only under load, conflation becomes a throughput mechanism. Ten receivers reporting target X within one flush window produce ten writes to the same header-keyed slot — nine are overwritten, one leaves. That is a content-free ~10× on a redundant feed, using only the header conflation key, and it de-risks the open pre-/post-dedup question below. Fan-out is the expensive stage because it multiplies by subscriber count, so every message collapsed before that stage is saved once per subscribed keyspace fraction.

What the client has to do — the consumer half

design proposal The relay-side contract is specified (meridian-9j8); the client-side twin below is derived from it here and is not yet a bead. It is the first thing this team should file.

  1. Hold last-known-state, keyed by conflation key — not a message queue. The client's model is a table of targets, not a log of updates.
  2. Age by header timestamp, never by arrival — staleness is a property of the observation, not of the delivery. This is what makes the picture correct across reconnects and route changes.
  3. Render staleness rather than hiding it — a target that has not updated in n seconds should look different from one that just did. Silence and a stale value are not the same thing, and the operator is the one who must be able to tell.
  4. Use sequence for gap detection, not for reassembly — a gap in a conflated stream is expected and benign; a gap in a volatile stream is data loss. Same field, two different meanings, decided by the declared contract.
  5. Beacons are the liveness channel, and they are separate — a health beacon (proposed 21002) says the platform is alive. Position silence says nothing new to report, or the link is down, and the client cannot distinguish them without the beacon. This is the closest thing we have to Last Will & Testament, and it is a beacon rather than a broker primitive precisely because we are time-based.
The open question this page used to carry is now answered — and made normative

It asked whether the 400–600k msg/s feed figure was pre- or post-dedup, because the answer moves egress sizing by ~10×. MIP-AS settles it: the deployment feed is pre-dedup at roughly 10× receiver redundancy — ~400k raw frames/s resolve to ~40k unique vessel states/s. Dedup is therefore the single highest-leverage operation in the path, it removes ~90% of every downstream cost, and it lives at the feeder because it compares content and the relay has forsworn content.

The spec does not just assert the ratio, it makes the deployment keep proving it: a feeder MUST publish meridian_ais_frames_received_total and meridian_ais_states_emitted_total, whose ratio is the dedup factor. That is normative precisely because a deployment which cannot answer this from its own metrics will answer it from memory and be wrong.

StageRateB/reportWireLabel
A — as observed today: MQTT JSON, pre-dedup, 4 topics400k/s23847.63 Gbpsmeasured payload size
B — MIP-OF format only, still pre-dedup400k/s680.218 Gbpsestimate 35×
C — plus feeder dedup at 10×40k/s680.022 Gbpsestimate 351×
D — plus spatial batching at 6440k/s32.70.011 Gbpstarget 729×

The three multipliers are independent and compose — format ×35, dedup ×10, batching ×2.1. Dedup is the cheapest of the three to implement and the second largest: do it first.

The correction that came with the answer — do not overstate the frame path

At 40k post-dedup states/s, ingest is not the problem on any path. Even the fully signed path costs only ~1.5 cores. So the argument for the opaque frame path at this workload is not ingest CPU, and the reverse claim — "we need MIP-OF because AIS ingest is too expensive to verify" — is false at 40k/s and would be caught by the first reviewer who does the arithmetic.

The three real reasons, in order: (a) zero-copy fan-out, the only term that scales with subscriber count — 2.2 cores of serde_json at N=64 versus one buffer and N refcounted sends; (b) key-expression interest scoping, which requires the routing key to sit in a header the relay can read without parsing; (c) 40k/s exceeds the stored-and-indexed ceiling (~10–30k/s), so this traffic must be ephemeral regardless of which path carries it.


8 The MIP register — 23 proposals, a lifecycle, and zero enforced

This section has grown more than any other since the page was written. It described four specs under an editorial naming convention; it now describes a governed register with a state machine, a CI guard, and a hard rule about what a relay may advertise. Read the lifecycle before the register — the register's size is a measure of design work, not of shipped work, and the two are easy to confuse in exactly the direction that flatters us.

NIP or MIP? — the deconfliction rule, and why it is spelling, not judgement

This page used to carry a constituency test — "does anyone outside this deployment want it?" — which required a judgement call on every proposal. MIP-RG replaced it with something mechanical: upstream Nostr NIPs are numeric (NIP-01, NIP-42); repo-local proposals are two uppercase letters (MIP-OF, MIP-AS). Digits and letters are disjoint, so the namespaces cannot collide and no registry negotiation with upstream is needed. Not MIP-0001: a numeric scheme invites the reader to assume ordering and completeness, and it conflicts on every concurrent proposal — a two-letter mnemonic is stable, greppable, and conflicts loudly rather than silently. MIP now expands to Meridian Implementation Possibility, and it is optional by construction exactly as an upstream NIP is — which is why an implementation has to be able to say which MIPs it actually honours.

docs/nips/ is retained only for profiles of genuinely upstream NIPs (17 files today); everything repo-local lives in docs/mips/.

The lifecycle — and the one state that permits advertisement

StateMeans
DRAFTProblem and wire shape stated. No code.
REVIEWCouncil seated; reversal evidence named.
IMPLEMENTABLEWire format frozen; golden vectors published.
ENFORCEDA reader exists; just check-kinds green.
WITHDRAWNReason recorded.
ENFORCED is the only state that permits NIP-11 advertisement — and this rule was bought with a real bug

NIP-17 was advertised for months while kind:10050 was rejected by the ingest allowlist. Conformant third-party clients feature-detected support, and then failed silently. A negotiation field that promises frames the relay will reject reproduces exactly that bug on the hot path. A relay advertises its MIPs in NIP-11 supported_extensions as mip-xx, and the operator console renders that list — so the register and what a relay claims are checkable against each other.

Every MIP below is DRAFT. None is ENFORCED. Nothing here may be advertised yet.

The register — 23 proposals, and what each owns

Mechanism MIPs add machinery. A profile MIP adds none — it binds an existing envelope, payload schema and link profile to one real-world feed, and states which enforcement point rejects a non-conforming frame. That distinction is what keeps the register from becoming a wish list.

MIPLayerOwnsBead
MIP-RG RegistryProcessNumbering, lifecycle, the advertisement rule above. Governs the rest.—
MIP-OF Opaque frame envelopeWireThe ~44 B header, batch framing, the opaque-payload contract, the conflation key. The load-bearing spec.tdq
MIP-XP Exchange profilesLinkThe P0–P3 ladder with per-class floors, negotiated once per link and enforced per message. Keeps transport auth and event identity as two layers.2e2
MIP-SF Session framesWireRemoves BIP-340 from the ephemeral hot path — after AUTH the WebSocket is the attribution channel. ~350× on the class that dominates message count. Adds a message type (SFRAME), not a kind, and no new crypto dependency.owl
MIP-QC QoS classesSchedulingEight lanes, class-aware shed, and the three movement contracts (§7).9j8
MIP-LF Leaf forwardingTopologyHub-and-leaf, store-and-forward, no peering — the topology in §3.—
MIP-MQ MQTT interopInteropTopic mapping, QoS mapping, session semantics. Identifiers travel as attributes, never mapped into the routing key by string manipulation.—
MIP-CU Custody + app ackDeliverySeparates the five things NIP-01 OK conflates: accepted / stored / authorized / delivered / acted on.8qkw
MIP-DD DDIL sessionsLinkResume for intermittent links, and four counters deliberately not collapsed into one — see below.bm32
MIP-AD Adapter contractIngestUnits, CRS, time base, declared lossiness. The reusable asset — it is what makes every profile below thin.qi7f
MIP-KN Kinematic payloadPayloadPosition, velocity, heading, altitude. One schema serves drones and surveillance — drones batch temporally, AIS/ADS-B spatially. Two schemas would mean every fusion implementation writes two parsers plus a converter, and the converter is where provenance gets lost.ss1
MIP-TL Scalar telemetryPayloadTyped readings — unit, value, quality, sensor id.—
MIP-MS Media sessionsMediaHow a relay learns continuous media exists and who may consume it — control plane only, bytes never on the event path.7xh3.1
MIP-CT Symbol identity + Compact Track CodeProfileSIDC/XSIDC on the backbone, and a compressed track code for disadvantaged links. This is §9, and it is the largest spec in the register.qtl8
MIP-AB ADS-BProfileThe cooperative air picture — 1090ES, UAT 978, GDL 90.768v
MIP-TK Cursor on Target / TAKProfileTAK interoperability. Note the tension with §9: CoT's fused type string is named there as the canonical example of what not to do — so this profile's job is to translate it at the adapter, never to adopt its shape.doty
MIP-AS AIS feedProfileThe first real workload on the machine surface — the reason the suite stops being inventory.hgjj
MIP-MI Motion imageryProfileSTANAG 4609.7xh3.1
MIP-ML MAVLink v1/v2ProfileThe autopilot protocol PX4, ArduPilot and effectively every open GCS speak.is42
MIP-RI Remote IDProfileASTM F3411 / prEN 4709-002.n4jr
MIP-DS ROS 2 / DDSProfile—fsjp
MIP-NM NMEA 0183Profile—fqeu
MIP-CN Channel canvasDocumentThe shared per-channel document, kind 40100. Read the entry below — this one arrived from the opposite direction to every other row.d7o

Layering is strict and one-directional: MIP-XP → MIP-OF → MIP-QC → MIP-LF → MIP-MQ, with MIP-SF, MIP-KN and MIP-TL hanging off the envelope. A payload MIP may never require the relay to read it — that is the enabling property, not a limitation.

MIP-CN is the law's mirror image — enforcement without a contract, and it did break something

Every other row in the register is a spec waiting for a reader. MIP-CN is a reader that had no spec. Kind 40100 was defined in kind.rs, enforced at relay ingest, and consumed by the ACP agent pool — with no written contract. Three independent readers each inferred one, and they did not agree: on two writes inside the same second, meridian-cli and the desktop rendered different documents, and neither was wrong by any rule that existed.

The page's standing law is no vocabulary without an enforcement point, and just check-kinds enforces it. MIP-CN is the converse failure, which nothing was checking: an enforcement point with no vocabulary written down. A CI guard can catch a kind that nothing reads; it cannot catch a kind that three things read differently. That one needs a spec, which is what MIP-CN now is.

Capability packs are a UI grouping, never a second registry

Onboarding offers domain packs — AIS, ADS-B, APP-6E, STANAG-4609, MAVLINK, TAK, REMOTE-ID — seven of them. A pack is a named bundle of MIPs a deployment has moved to ENFORCED. Partial support is not support: a pack that lights up while one member MIP is still DRAFT is the NIP-17 failure above wearing a friendlier label. just check-mips regenerates the client's registry from docs/mips/README.md and fails when the two disagree, so the register stays the one registry.

Independent convergence — record it as reversal evidence, not as conformance

On 2026-08-06 an external architecture review with no access to this repository independently derived the wire shape of ten registered MIPs — the binary envelope, negotiated per-link auth, no-signature-per-sample, eight delivery classes, fusion as ordinary clients, client-side symbology, media outside the payload path, and both payload schemas. It converged on Design Law too: opaque routing keys with no human-readable mission names, and a hot path that calls no store synchronously.

Two designs agreeing raises the cost of re-litigating the wire shape. It says nothing about whether a reader exists — only ENFORCED says that. The review's two genuine gaps became MIP-DD and MIP-CU; its other two proposals — a second durable event journal and per-region provisioning counts — were withdrawn under refusals R2 and R8, by agreement with the reviewer.

MIP-DD's four counters — the most likely implementation error in the register

All four look like "a version number" and only one is absorbing. Collapsing them is how revocation dies at reconnection, because a disconnected client is the second node, arriving early: it holds writes made while partitioned, and merging them by created_at resurrects anything revoked during the outage.

session_epoch reconnect incarnations, assigned by the relay · source_epoch source restarts, assigned by the source · policy_generation authorization version, authority only, monotonic · revocation_floor minimum acceptable generation, authority only, monotonic and absorbing.

A client is never authoritative for policy_generation. It reports the last generation it observed; the authority supplies the signed current policy. A resume that trusts the client's claim has invented a second authorizer, which the no-third-shim law refuses by name. Resume ordering is normative and policy-and-revocation delta comes before any data frame — a relay that delivers state first is non-conformant. The governing invariant: historical repair must never delay or temporally overwrite current state. Backfill is additive to the past, never to the present.

What MIP-DD deliberately does not decide: how revocation_floor advances, who signs it, and how it propagates. That is a Design Law change and goes to council before this MIP may reach IMPLEMENTABLE.

The mandatory, non-defaulting field in MIP-KN

provenance: self-reported | observed. A drone signs its own telemetry; an ADS-B target signs nothing and the feeder attests on its behalf. That distinction must not be droppable by omission — no default value, ever.

The companion honesty rule: fidelity: real means "not simulated", not "trustworthy". An unauthenticated broadcast faithfully relayed is still real and still spoofable. And spoof detection is an explicit non-goal of the transport layer — doubly so now, because a relay that never reads the payload categorically cannot detect an implausible track. Detection belongs to fusion. Stating that is not a caveat; it is the honest description of a relay that has forsworn reading.

Machine kinds — the block moved, and the axis changed with it

This page previously proposed 21000/21001/21002 split by payload type — kinematic frame, telemetry frame, health beacon (that proposal survives on meridian-41a and is now superseded). The register consolidated on a single ephemeral machine block, 24400–24419, and split it by symbol set — that is, by domain:

KindConstantDomainNominal refreshSpec
24400KIND_AIS_POSITIONAIS vessel position~0.1–2 Hz × 10⁵MIP-AS
24401KIND_AIS_STATICvessel static / voyage — carries the durable IMOlowMIP-AS
24402KIND_AIS_AID_TO_NAVaid to navigation — kinematic, zero velocitylowMIP-AS
24403KIND_TRACK_AIRair1–10 HzMIP-CT
24404KIND_TRACK_LANDland0.1–1 HzMIP-CT
24405KIND_TRACK_SEA_SURFACEsea surface0.1–1 HzMIP-CT
24406KIND_TRACK_SUBSURFACEsubsurface~0.01 HzMIP-CT
24407KIND_TRACK_SPACEspacesnapshot onlyMIP-CT

All five track kinds are conflated. Why domain and not payload type: air refreshes at 1–10 Hz and subsurface at ~0.01 Hz — three orders of magnitude apart — so the domain genuinely is a traffic class, which is sanctioned axis A4. Splitting by payload type would have put a 10 Hz and a 0.01 Hz stream in one lane. 24400 is a narrower profile of 24405, not a competitor: AIS is one source of sea-surface tracks, so a consumer wanting all surface tracks subscribes to both. KIND_TRACK_SPACE carries a hard consumer obligation — orbital motion MUST NOT be dead-reckoned; course and speed encode unknown by construction.

Hard constraint — this is the whole ballgame on cost

These kinds MUST be allocated in the ephemeral range 20000–29999, and 24400–24419 is inside it. MIP-SF's fence is a compile-time assert!(is_ephemeral(k)) per eligible kind, so a persistent kind number forfeits session frames and pays 30–70 µs of BIP-340 per sample instead of ~0.2 µs of HMAC — a ~350× difference decided entirely by which integer someone picks. A persistent kind on a session frame is rejected, never upgraded, because extending session frames to persistent kinds would convert "the relay can lie about who is typing" into "the relay can forge chat history."

State of the block in kind.rs today: none of 24400–24407 is committed yet. The MIP-AS three were authored, compile- and guard-verified, then deliberately backed out (meridian-bjgs) because a second agent was editing the same file — splitting one file across two agents would have committed their incomplete work and pushed a red just check-kinds. Re-apply once kind.rs is quiet.

Ephemeral kinds actually allocated today: 20001 presence, 20002 typing, 20003 media-session heartbeat, 22242 NIP-42 auth, 24134 pairing, 24200 agent observer, 24242 Blossom auth, 24243 identity binding, 24810 huddle reactions, 27235 HTTP auth, 28936 NIP-43 leave request. just check-kinds is green.

A worked example of why a BASELINE is a snapshot, not a fact. Gauntlet row G02 pinned 133 kind constants at 133 distinct values and still says so. The tree has since moved: it now declares 137 constants matching the guard's own pattern, at 136 distinct values — and that last number is not a duplicate-kind defect, it is PARAM_REPLACEABLE_KIND_MIN sharing 30000 with KIND_FOLLOW_SET, a range bound coinciding with the first kind in its range. The guard stays green because it checks readers and duplicates, not a hand-written total. Cite the register for what it pinned and when; re-derive anything you need to be current.

Three dependencies that are easy to miss

meridian-06p — stream labels are a safety primitive

Three closed lanes applied per stream, never per message: fidelity ∈ {notional, simulated, real}, context ∈ {experiment, exercise, operational}, access ∈ {community, organization, controlled}. This is the mechanism that stops a simulated track entering an operational picture. Per-message marking produced 70–84% defect rates in DoD IG/GAO audits; per-stream is cheaper and more correct, and costs zero per event because it resolves at subscribe time. Filed under "governance" it looks optional; filed correctly it is part of the machine bus.

meridian-luy — the recorder is unowned

It appears in three separate documents — drone post-flight reconstruction, AIS history, general cold storage — every time as someone else's job. Ephemeral kinds mean never stored, and the recorder is what answers the post-flight-analysis objection without putting telemetry in a persistent range. Shape: subscribe to the live stream, write durable signed segments out of band, store opaque blobs plus headers and decode lazily. Columnar/object storage, not Postgres — ~60k unique states/s at ~80 B is roughly 415 GB/day raw. The recorder is the signed P3 boundary; the live bus never is. Still open, still P2, still unowned — the one condition on this page that has not moved.

meridian-7xh3 — drone video is a P0 epic, and it is two builds

Stated need: "we need media support for the drones — video." Video as a chat attachment already works (meridian-media, Blossom/S3, desktop imeta). What does not exist: no NIP-71 video kinds, no NIP-68 picture kind, no NIP-53 live-streaming kinds. Two materially different builds hide under one phrase — (a) a live ISR feed (NIP-53 live event plus HLS or WebRTC; the relay already hosts huddle audio, so there is transport precedent) and (b) recorded post-mission clips (NIP-71 addressable video over existing Blossom storage). MIP-MS and MIP-MI are the specs, and the bytes stay off the event path in both.

The trust note that must be written down, not inherited (meridian-k6b)

MIP-SF's own trust table concedes that the relay can fabricate ephemeral events attributed to a client — and shrugs, because the worked example is a typing indicator. Applied to a kinematic frame, the same capability means the relay can inject targets into a tracking picture. It is arguably not a new exposure, since a P0 feeder link has the same property. But inheriting that reasoning silently from a document about typing indicators is exactly how it never gets written down. It lands as a section of MIP-KN.


9 Symbology — SIDC / MIL-STD-2525 / APP-6, and the kinematics extension

Status — this section is no longer the blank page it used to be

This page previously reported that .settings/reference-docs/sidc-2525-APP6/ was empty, that no symbology bead existed, and that nothing was decided. All three are now false. MIP-CT (meridian-qtl8) is written and is the largest spec in the register; the reference directory holds the CTC calculator, record format v3.1, which is the normative reference for every field width, band table and hydration rule; and the gauntlet carries symbology as row G08.

And it is no longer only a spec — a client is being built against it. meridian-9qll is the tactical-picture epic: an Apps region in the left nav keyed off the MIP register (landed), a Map app on MapLibre GL v5 with 2D and globe modes, a milsymbol renderer bound to XSIDC with amplifiers, an XSIDC track store that conflates by instance, and an honest degraded/empty state. APP-6 hulls for AIS, a zoom detail ladder, clustering, per-layer opacity, table export, Esri Shapefile export and a motion-imagery video wall have landed alongside it. The epic is still 1 of 6 children complete — the shipped surface has outrun its own acceptance criteria — and gauntlet rows G06 and G08 remain UNSTARTED. Shipping UI does not move a gate; evidence does.

G14 is the exception, and it moved the right way. It reached SLICE on named tests for mapping, symbology, degraded/staleness rendering and tactical UX — and in the same pass the word alerts was cut from the row's title. A search found the string twice under the apps tree, both times a lucide AlertTriangle icon: no alert rule, geofence, proximity, tripwire or watchbox concept exists anywhere in REMAPPING/meridian-desktop/src/ or crates/, and kind.rs declares no alert kind, so nothing could carry one over the wire. Rather than let the row keep claiming a capability nobody had built, the capability was scoped out and its absence written down (meridian-in50) — which is the honest version of a green gate, and worth more than the gate.

What follows is what MIP-CT decided, including the one place it overturned this page's earlier recommendation, and the one question it still leaves open.

The gap the client surfaced, which the spec had not

The desktop rendered the chat NIP surface and nothing else, so a deployment that moved MIP-CT to ENFORCED would have had no way to see the tracks those MIPs carry — MIP state was visible only as a read-only Settings card. That is the "no vocabulary without an enforcement point" law reaching its natural conclusion: a reader is not only a parser in the relay, it is eventually a human looking at a screen. meridian-9qll.1 is the concrete consequence. MIP-CT § 6.5 now specifies the full 22-field APP-6 text-amplifier extension — including the commonly omitted AG · Auxiliary Equipment Indicator — while its writer/validator/decoder remains to be implemented.

The two layers — and why there are two

XSIDC — the canonical vocabulary

Full fidelity, carried on the backbone: the MIP-OF frame is a 44 B header plus a full-fidelity payload. This is the interior representation, and it is what fusion, recorders and displays consume.

CTC — the Compact Track Code

A quantized, band-tabled record for genuinely disadvantaged links. It hydrates back to a track using mission defaults. Never treat a hydrated CTC record as equal in fidelity to the record it came from — the loss is quantified, not hand-waved, and the CTC profile explicitly drops both source attachment and the complete text-amplifier group.

Q1 · Header or payload? — answered, and this page had it partly wrong

The earlier answer was a clean blanket rule: the SIDC struct belongs in the payload, never in the header. MIP-CT keeps the reasoning and rejects the blanket. The discriminator is not "is it symbology" — it is mutability.

SIDC elementMutable?PlacementWhy
Symbol set (pos 5–6) — air, land, sea surface, subsurface, spaceeffectively immutable for a platformHeader, via kindDomains have genuinely different QoS profiles — air 1–10 Hz, subsurface ~0.01 Hz, space a snapshot that must not be dead-reckoned. That is traffic class — sanctioned axis A4, not vocabulary sharding
Standard identity (pos 4) — pending, unknown, friend, suspect, hostilehighly mutable — re-assessment is the normal casePayloadAn affiliation in the routing key means re-classifying a track changes its routing key — with the added edge that the track would vanish and reappear on a subscriber's display at the moment of re-classification
Entity code (pos 11–16)refined as classification improvesPayloadSame argument, plus it is a type code with ~10⁶ values; it cannot be a routing dimension
Version, context, HQ/TF, echelon, modifiersmission-constantNeither — mission configNot sent on any link
Instance identifierimmutable for the track's lifeHeader — conflation keyThis is what conflation supersedes on. On the backbone it is the full identifier, never the CTC alias
Position—Header — geo cell and payloadThe cell routes; the payload carries the measurement
Observation time—Header — timestampLWW resolves on it
The one-line rule that replaces the old blanket one

The relay routes by where a thing is and what domain it is in. It never routes by what someone currently believes about it.

Ten of the thirty digits are information-bearing and travel; 10¹⁰ < 2³⁴, so the wire base is a 34-bit field plus a sidc_defaults_version naming which mission table restores the other twenty.

The named prior art this gets right by getting it wrong

Cursor-on-Target's type="a-f-A-M-F-Q" fuses affiliation (f) into the type string that TAK routing is built from. That is exactly the failure above, shipped and widely deployed — which is why the mutability rule is written as law here rather than left to reviewer judgement.

Q2 · What the payload carries

GroupFields
Identitysidc_wire (the 10 digits as one 34-bit field) and sidc_defaults_version
Kinematic (MIP-KN)latitude, longitude, elevation, course, speed, plus their measured accuracies — unquantized
Track qualityphase (STANAG 4817 TrackPhase: tracked / dead-reckoned / lost / inactive) and confidence
Provenanceprovenance — mandatory, non-defaulting (self-reported | observed)
Source attachmentsource_format, source_bytes
Payload extensionsindependently length-delimited groups; extension 1, version 1, carries all 22 APP-6 text amplifiers

Three design calls worth knowing before you argue with them. (1) Track phase is not SIDC status. SIDC status (pos 7) is operational condition — present, planned, damaged, destroyed. Phase is track quality. A destroyed target can still be a well-tracked object, so both travel. (2) Confidence has no "unknown" codepoint — ten buckets, and an unsourced confidence still picks one: the lowest defensible. Confidence ≤ 0.35 SHOULD pair with standard identity Pending or Unknown. (3) Elevation is f32 metres, not fixed-point — the kinematic core spans crush depth to beyond GEO, eleven orders of magnitude, and a fixed-point integer that reaches 36 000 km cannot also resolve a drone at 840 m. Relative precision is what elevation needs.

Byte budget: ≤37 B fixed-width core plus the empty extension-vector byte, so header plus bare payload is ≤82 B, landing near ~76 B after postcard varints — for a track carried without source attachment or populated payload extensions. Quoting 76 B for an attachment- or amplifier-bearing track would be exactly the profile-free figure rule ① forbids.

Q3 · Adapters — additive, never destructive

SIDC is a symbology vocabulary, not a superset of every source data model

A SAPIENT detection carries sensor-level structure — classification vectors, detection geometry, sensor identity — that no SIDC digit can express. Discarding it at the adapter makes the transform irreversible and silently lossy. So: an adapter MUST carry the source record's original bytes in source_bytes, tagged with source_format, whenever the source is not itself XSIDC. The backbone has the bandwidth and the relay never reads it, so the cost is bytes and nothing else — and what it buys is an auditable transform and a mis-mapped adapter that is diagnosable after the fact rather than a permanent loss.

The transform itself is a table lookup, never a string transformation: a source format's identifiers, topic strings and platform names travel as attributes and are never mapped into the routing key by string manipulation.

SourceWhat the adapter must supplyCoverage today
STANAG 4817 — the NATO track/contact model this layer compresses and reconstructs; the closest thing to a native input4817 track fields → SIDC digits + extension; phase maps directly, since TrackPhase is 4817'sgauntlet G07 UNSTARTED
SAPIENT — sensor-level detections from modular autonomous sensorsDetection → track association. This is a fusion step, not a format conversionF005 — prose in MIP-CT only; no adapter, mapping or fixture
AISship type → entity code; MMSI → instance identifierMIP-AS written
ADS-Bemitter category → entity code; ICAO24 → instance identifier; provenance = observedno ADS-B profile MIP yet — the pack binds through MIP-AD
STANAG 4559 — ISR library discovery and retrieval—F004 — zero coverage in any MIP, crate, spec or fixture

Q4 · Trap 2 is the one thing here still genuinely open

Two places to say "this is a simulation" — narrowed, not closed

The risk was that 2525/APP-6's own exercise/simulation amplifier and our fidelity/context stream labels (meridian-06p) would drift, silently and safety-relevantly. MIP-CT narrows it substantially: the SIDC Context digit (pos 3 — Reality / Exercise / Simulation) is mission-wide with no per-record override and never travels on the wire at all. So the per-frame contradiction this page feared cannot occur.

What is left is smaller and still real. The mission table and the stream label remain two independent assertions of the same fact, at two different scopes, and MIP-CT does not cite meridian-06p or say which one wins. MIP-CT adds its own sharp warning in the same breath — an exercise track that inherits 0 - Reality is indistinguishable from a live one, so 1 - Exercise is the correct default for any non-operational mission.

The recommendation stands, unchanged and still unratified: the stream label is authoritative — it is enforced, closed-enum, CI-checked, and resolved at subscribe time at zero per-event cost — and the mission-table context is derived from it, never read back as an independent source of truth. This is the one genuinely new decision this document surfaces, and it belongs to the Protocol council, not to a recommendation in a slide.

Q5 · The custom kinematics extension

What is still owed here
  1. Name the standard revisions actually in scope. MIP-CT works in 2525E (version 13) with 2525D/APP-6(D) as version 10. Which do consumers actually render? That answer still sizes the client work, and it is not written down.
  2. Take Trap 2 to the Protocol council with reversal evidence, and record the call in the bead. Still cheap now, still expensive after either side ships.
  3. Close F004, F005 and F006 — STANAG 4559 has no coverage at all, SAPIENT is prose only, and there is no adapter SDK, so every standards mapping would otherwise be a bespoke crate with no shared contract.

10 Where to start — the plan of record

The binding constraint on this program is WIP, not knowledge, and it has grown since this page was first written — and it is still growing faster than it drains. Live tracker state: 485 issues total — 305 open, 235 ready to work, 16 in progress, 70 blocked, 164 closed.

The trend is the finding, not the snapshot

Across the three days this page has been reconciled, the tracker went 303 → 402 → 485 issues and 139 → 197 → 235 ready. It closed 39 then 38 in those two days, and opened 99 then 83. The backlog is growing roughly twice as fast as it drains, and the ready queue has grown 69% while in-progress held flat at 15–16.

Two hundred and thirty-five ready items is not a plan; it is a menu that takes longer to read than most of its entries take to do. Pick one track, and start a second only when the first has shipped something. This is the Outsider's seat in §11, and it is the one warning on this page that has got worse at every single reconciliation.

The plan of record is now TASKS.md, and it is a 10-week program

This section used to carry a hand-ordered bead list. That list has been superseded by a costed program plan that executes the ratified council call in DIAGRAM.md § Part III and does not re-argue it. Beads remains the source of truth for task state — TASKS.md mirrors Bead IDs, never the reverse — and the JIRA project key is MRDN.

Three assumptions in it require confirmation before Sprint 1 planning, and each changes sizing rather than direction: a 6-engineer team at ~55 points/sprint (~260 points total); one dedicated 16-core load-generation host and one 16-core system-under-test host with ≥25 GbE between them — the NIC is a first-class dependency here, not an afterthought; and the fact that the 1.337M scoping figure is TBMQ-class vendor-published, not measured on our stack.

Phasing — greenfield core first, bridge second, and the order is the point

WeeksTrack
1–6Greenfield opaque-frame core. Native MIP frames end to end. Hits the throughput number on a path with no MQTT semantics in it
6–10MQTT compatibility bridge. Ingress adapter at the edge, so devices that cannot be reflashed migrate without firmware work

Building the bridge first would let MQTT semantics — per-message topic ACLs, broker-held delivery state, topic-string tenancy — leak into the core, which is precisely what this program exists to remove.

The eight epics

EpicTitlePriPtsSprints
MRDN-100Measurement baseline and capacity model — the gate; nothing optimizes before this has a numberP0341
MRDN-200MIP specification suiteP0471–4
MRDN-300Opaque frame data planeP0632–5
MRDN-400Traffic class, QoS, and congestionP1343–4
MRDN-500Hub-and-leaf topology and store-and-forwardP1423–5
MRDN-600MQTT compatibility bridgeP1473–5
MRDN-700Client SDK and operator consoleP2342–5
MRDN-800Conformance, load, and handoff — runs continuouslyP0421–5
343 points against a 260-point budget — deliberate, and it must be resolved at Sprint 1 planning

Not discovered in Sprint 4. The named cut line: E7 drops to CLI-only, E6 descopes MQTT 5.0 to 3.1.1, and the MIP-TL payload schema defers — which leaves 258. Do not resolve it by cutting E1 or E8. Those are the gates that make every other number on this page true.

Sprints, and the one gate that can stop the program

SprintThemeExit gate
S1 · wk 1–2Measure and specifyBaseline numbers published; MIP-OF and MIP-XP at IMPLEMENTABLE; go/no-go on the whole program
S2 · wk 3–4Frame path existsCodec fuzzed and golden-vectored; relay parses and routes a frame end to end
S3 · wk 5–6Batch and classBatch survives to fan-out; 8 QoS lanes wired; class-aware shed proven under a slow consumer
S4 · wk 7–8Edge and interopLeaf relay forwards upstream with store-and-forward; MQTT 3.1.1 bridge publishes into the spine
S5 · wk 9–10Prove and hand offSustained-rate capacity run with attached profile; conformance suite green; handoff frozen
Sprint 1 is a go/no-go, not a checkpoint

If the Sprint 1 measurements show the opaque-frame path is dominated by something unlisted — a TLS setting, an accidental debug! in the fan-out loop, a clone() on a hot struct — then the batching and QoS work in S3 is optimizing the wrong term and must be re-planned before it starts. The three named outcomes:

MeasurementIf it landsIf it does not
Per-message frame-path cost within ~2× of the 1–3 µs estimateproceed as plannedre-scope E3 around the actual dominant term
Batching projects ≥10× on pipeline traversal countproceed with MRDN-310drop meridian-0zd; find the real cost
Zenoh ≥5× over Dragonfly on one fixed topology (meridian-ctg)Zenoh becomes a candidatestay on Dragonfly; defer the whole implementation chain

Ten constraints a story is rejected for violating, regardless of its benchmark

C-1 every published figure carries its profile · C-2 no vocabulary without an enforcement point · C-3 names never encode ownership — no org/site/device prefixes in key expressions, storage keys or ACLs · C-4 add attributes, not mechanisms — no policy DSL, rules engine or plugin runtime in the bridge · C-5 no third shim — the bridge holds no ACL copy and no sync job · C-6 explicit denial, never a silent drop · C-7 revocation is monotonic — no second writer or peering link before meridian-3wh · C-8 nothing closes on green CI alone · C-9 scale by replica / function / tenant / traffic class, never by vocabulary · C-10 measure before optimizing; machinery for absent load is inventory.

And four refusals this program will be asked to reverse, named now so they are declined by citation rather than re-argued: "put Kafka behind the relay for durability" (R2) · "shard telemetry kinds onto their own relays" (R1) · "let the edge relay peer with the spine" (R4 / Law 2) · "cache grants at the bridge for speed" (R5 / C-5).

Parked — designed and sized, waiting on a named trigger

Parking is not rejection. Each of these is designed, sized and cross-linked, and ready the day a driver appears:

ParkedUn-parks when…
Grants, discovery loop, restriction-drift metera named party wants cross-organisation sharing
Grant-keyed envelope encryptiona stream must genuinely exclude a member of its own community
Participant classesthe first external / partner tenant
Federation, negentropy backfilla second network that wants to federate
Compliance criteriasomeone asks us to prove conformance
Tasking orders · fusion productsa platform to task, or a second sensor to fuse

Day one — get the stack up

. ./bin/activate-hermit     # pinned toolchain — do this first, always
cp .env.example .env
./run.sh doctor            # toolchain, Docker, port conflicts, stale processes, env drift
./run.sh all               # services + relay + web in background, desktop in foreground
just ci                    # the full local gate — run before every PR

# The four guards that keep this page from rotting. Read them before trusting it.
just check-gauntlet        # every G-row's state against its evidence
just check-mips            # the MIP register against the client's generated registry
just check-kinds           # no vocabulary without an enforcement point
just check-architecture-map # the crate map, asserted in both directions

bd ready                   # what is actually available to work on
bd show meridian-tdq       # read any bead cited on this page

just bench                 # ALL seven bench groups, and it prints the profile with
                           #   them — CPU, cores, OS, traffic class, auth profile,
                           #   parsed/stored — so no figure can be lifted out bare
just bench --quick         # same groups, shorter sample
./perf/relay_bus_scaling.py --mode redis   # interest-scoping reduction

Prefer just bench over a bare cargo bench. The recipe exists precisely because cargo bench appeared in no recipe and no workflow while its numbers were being published (finding F062), and because a figure without its profile is an unsupported claim under rule ①.

Commit with git commit -s — the DCO check fails any PR with a commit missing a Signed-off-by trailer. run.sh wraps just and adds port deconfliction; it does not replace it.


11 Council record — review of this document

Convened per AGENTS.md § User Preferences. Decision under review: publish this page as the standing introduction for the incoming Program Development Team, framing Meridian as a modular replacement for a TBMQ central-broker deployment and as a scalable alternative for decentralized adoption. Each seat gives a position, one falsifiable objection, and what would change it.

SeatPositionStrongest objection / named riskWhat would change it
Contrarian approve, amended "Drop-in replacement" is the phrase that will get quoted back to us in a procurement meeting, and the very next question is offline queueing for constrained devices — which we do not have and cannot add without a second durable log. Amendment applied: §3 leads with the narrowed claim (bus, policy, audit role) and gives the gaps their own table before the argument table. Second point, also applied: §9 stated plainly that no symbology design existed rather than implying one. Since ratified: that narrowing is now written law in TASKS.md § 1 as a standing architectural boundary with its own kept-at-the-edge column, and §9's gap has closed — MIP-CT exists, so §9 now reports a design rather than a blank page. A customer workload dominated by intermittently-connected devices — then the broker is being kept, not replaced, and this becomes an integration document.
First Principles Thinker approve Strip it back: the relay moves bytes to interested parties with an identity attached. The page must sort the same way — throughput and interest routing are the product; governance is a feature — with the one exception that stream labels are safety, not governance, and belong on the product side. Applied: §8 carries the reclassification explicitly, and §9 Trap 2 makes the label authoritative over the symbology amplifier for exactly that reason. Update: Trap 2 has narrowed but not closed — MIP-CT keeps the SIDC context digit off the wire entirely, so the per-frame contradiction cannot occur, but the mission table and the stream label remain two assertions of one fact and MIP-CT does not say which wins. Evidence that a renderer cannot honour a stream label — then the authority direction in Trap 2 inverts and the recommendation is wrong.
Expansionist approve The reusable asset is not the AIS path, it is the adapter contract — every high-rate feed terminates the same way, so the second feed should be configuration. The page under-sold that in draft. Applied: §6 opens on the adapter contract as six numbered obligations, and MIP-OF is described as the asset rather than as a spec. Self-check honoured: federation still has no second party, so §4 marks A5 blocked rather than upcoming. Vindicated, and then some: the adapter contract is now its own spec — MIP-AD — with five protocol profiles already registered against it (MAVLink, NMEA 0183, Remote ID, ROS 2/DDS, AIS), which is precisely the "second feed is configuration" shape. The reversal evidence has arrived in the form this seat named: MIP-AD is being designed against several callers rather than one, which is better — so the sequencing question is now which profile is first, not whether the envelope generalizes.
Outsider approve, amended Three things a newcomer will not otherwise be told: the recorder is the hidden common dependency and nobody owns it; the two-ceilings problem is commercial, not technical, and is the cheapest item on the list to get right and the most expensive to get wrong; and five competing programs against nine in-progress items is the organisation's actual constraint. Applied: the recorder gets its own card with the 415 GB/day sizing, §0 makes the ceilings rule the first thing on the page, and §10 opens with live tracker counts rather than the plan-of-record snapshot. This seat's warning has aged the worst, which is to say the best: WIP has grown from 50 open / 9 in progress to 201 open / 15 in progress / 62 blocked, and the recorder is still unowned. Someone taking ownership of the recorder — then it stops being a hidden dependency and moves into the ordered track. Not yet: meridian-luy is still open, still P2, still unowned.
Executor condition cleared A page this comprehensive risks becoming the deliverable. The failure mode of the whole GOAT exercise was a beautiful plan nobody builds — five documents, twenty-five issues, zero lines of code. Condition applied: §10 is an ordered track with gates, not a menu, and §9 closes with concrete artefacts to produce before any code. Condition was: the symbology work was in no tracker, so bd ready could not surface it. It has cleared twice over — meridian-qtl8 (MIP-CT) and the meridian-9qll client epic are filed, symbology is gauntlet row G08, and a MapLibre/milsymbol Map app is being built. Restated concern, and it has got worse at every reconciliation: 23 MIPs are DRAFT and zero are ENFORCED; no gauntlet row has reached PASSED; and over two days the backlog grew 99 then 83 while closing 39 then 38. Specification is outrunning enforcement, which is this seat's failure mode wearing a register instead of five documents. The counter-evidence, and it is real: G14 reached SLICE on named tests and cut an unbuilt capability out of its own title rather than carry it — that is the loop working exactly as this seat wanted. The first MIP reaching ENFORCED with a reader and a green just check-kinds — that is what converts the register from plan to product. Until then, treat the register's size as a risk indicator, not a progress indicator.
Sr. Full Stack Developer approve Sizing gets underestimated in exactly one direction, so the page must not imply that §8's specs are comparable in cost: lq7/06p are days, 0zd is weeks and touches 21 mpsc sites in the hottest path in the relay, and grants/encryption/federation are a quarter each. Applied, then superseded by something better: §10 no longer carries a hand-written size column — it carries story points per epic, and the plan admits up front that it is 343 points against a 260-point budget with a named cut line. That is the honest version of the same concern. One precision I want kept: the desktop e2e suite does not cover connection.rs — the relevant safety net for the batching refactor is relay-level fan-out coverage, which is a different suite, and conflating them is how a refactor ships without a net. Relay-level fan-out coverage landing before 0zd starts — that is the condition, and it is already pinned by two mutation-verified tests.
Network Engineer & Architect approve Two architecture facts must be owned explicitly rather than discovered. One: we are building two relays in one binary, and that should be a deliberate module boundary — §2 now leads the entire document with it. Two: the metric that judges every roadmap item is does it improve interest scoping?, because egress is the binding constraint and f_s is the only term we control — §5 carries the equation and the three-scenario table. Sequencing is already correct: Phase 0 measurement gates the rest; leave it. Both have hardened since: "two relays in one binary" is now "two edges, one relay" in ARCHITECTURE.md, with the machine edge speaking Zenoh natively rather than NIP-01 — and the egress point is now carried by a measured ladder in §7 (format ×35, dedup ×10, batching ×2.1, composing to 729×). A measurement showing pipeline overhead, not egress, dominating at realistic subscriber counts — then the optimisation order inverts.
Chairman — the call, and what has moved since it was made

Approve. The original outstanding condition has cleared, and one has replaced it. Publish this as the standing introduction. The Contrarian's amendment is the load-bearing one and is applied: the claim is modular replacement of the broker's bus, policy and audit role, with the gaps given their own table before the argument for the replacement, and a broker legitimate — often correct — at the device edge, feeding the spine and never routing for it. Every figure carries its profile and its measured-or-estimated status. No head-to-head throughput claim is made anywhere on the page, because no head-to-head benchmark exists.

The First Principles sort is adopted: throughput and interest routing are the product; governance is a feature — except stream labels, which are safety and move to the product side. That sort is what decides §9's Trap 2, and Trap 2 is still the one genuinely new decision this document surfaces — narrowed by MIP-CT, not closed by it — so it goes to the Protocol council as its own item rather than being settled here by a recommendation in a slide.

Condition cleared (Executor): the symbology work is now in the tracker (meridian-qtl8, meridian-9qll, gauntlet row G08), MIP-CT is written, and a client is being built against it. Condition now outstanding, and it is the same failure mode one register later: the MIP register has grown to 23 proposals and not one has reached ENFORCED; the gauntlet has 21 rows and not one has reached PASSED; and the backlog grew by 99 then 83 issues across the two days this page was last reconciled. Move one MIP to ENFORCED and one gauntlet row to PASSED before adding a twenty-fourth MIP, or the register becomes the deliverable. The condition was stated as "before a twenty-third" one reconciliation ago; the twenty-third arrived first, which is the datum.

Reversal evidence — any one of these reopens the call:

  1. A workload profile dominated by intermittently-connected devices needing broker-held offline queues. Then the broker is not being replaced; it is being kept, and this becomes an integration document. Partially addressed since: MIP-DD specifies session resume, which is not a queue and must not be sold as one.
  2. Zenoh Phase 0 (ctg) measuring under 5× against the current bus on the named profile. Then §6 loses its premise and the transport chain defers. Still unrun — this is the live gate, and it is Sprint 1's third named outcome.
  3. Postgres stored-event throughput measuring materially below the ~10–30k/s estimate. Then §3's argument 2 becomes an argument against us until the write path is fixed.
  4. A renderer that cannot honour a stream label, inverting the authority direction in §9 Trap 2.
  5. Retired. The pre-/post-dedup question was reversal evidence when this call was made, because it moved egress sizing by ~10×. MIP-AS answered it — pre-dedup, ~10× receiver redundancy — and made the ratio a metric the deployment must keep publishing rather than a fact it may misremember.