CrowdVision · source-available, not open source · © 2026 Nicolò Ghignatti
Bounded context: Telemetry Ingestion · Stack: Rust / Axum / Postgres + TimescaleDB · Code-level walkthrough: Telemetry Service
The telemetry realises the Telemetry Ingestion context. It is the platform’s ingestion gateway, and its architecture answers two independent pressures: absorbing new sensor types without touching the core, and keeping every I/O technology out of the logic that decides what a reading means.
Hexagonal (ports & adapters) and microkernel on orthogonal axes. They are not competing patterns here; each governs a different direction of change.
| Axis | Pattern | Varies |
|---|---|---|
| Outward — infrastructure | Ports & adapters | Postgres, Redis, Kafka, digital-twin, physical devices |
| Inward — domain | Microkernel | which metrics exist, what they validate, which bounds they carry |
The Node predecessor had the microkernel half only. Its modules owned their own persistence and Redis calls, so infrastructure reached into every metric implementation.
| Directory | Holds | May import |
|---|---|---|
src/types/ | pure types, SensorPlugin and ActionSpec, threshold and query rules | nothing |
src/kernel/ | use cases over Arc<dyn Port> — the microkernel | contracts |
src/plugins/ | one file per metric, each a SensorPlugin | contracts |
src/adapters/ | Postgres, Redis, Kafka, HTTP in and out | everything |
The kernel never names a plugin. Every metric-specific decision is reached through a &dyn SensorPlugin resolved from PluginRegistry, so a new metric is a new file plus one registration line in main.rs.
tests/architecture.rs enforces the table: no crate::plugins or crate::adapters under src/kernel, no I/O crate under src/kernel or src/types, and no plugin importing a sibling.
graph TD
subgraph Driving
HTTP[HTTP API]
KC[Kafka registration consumer]
end
subgraph Kernel
K[Use cases: ingest, readings, thresholds, sensors, actions, registration]
R[PluginRegistry]
end
subgraph Plugins
T[temperature]
P[peopleCount]
A[airQuality]
end
subgraph Driven
PG[(Postgres + Timescale)]
RD{{Redis pub/sub}}
KP[Kafka producer]
TW[digital-twin directory]
DEV[Device ACL]
end
HTTP --> K
KC --> K
K --> R
R --> T
R --> P
R --> A
K --> PG
K --> RD
K --> KP
K --> DEV
HTTP -.->|authz| TWMetricDescriptor names the fields, their kinds, the unit, and which field becomes value; bounds() names the threshold keys; actions() names what the metric can be told to do. GET /contracts and POST /ingest are driven by the same declaration, so the catalog and the validator cannot drift.Arc<dyn ReadingStore>, not a PgPool. The whole kernel is unit-tested against in-memory fakes with no database in sight.room_id NULL means building-level; a room-level row wins during resolution. Room thresholds cannot be silently dropped.PATCH /thresholds/{sensorType}/buildings/{id}/rooms takes a map of room id to bounds. Registering a building sets a threshold for every room at once, and one request per room made that both the bulk of the round trips — each paying its own edge auth — and partially appliable. Every patch is validated before any is written, so a rejected bound stores nothing. The writes are still one upsert per room: the store has no transactional bulk write, and a mid-write failure is rare and idempotent to retry.ActionDispatch takes a Command in our own vocabulary; URLs, methods and field names exist only in the dispatch adapter. See Sensor Actions & the Device ACL.telemetry-db (Postgres + TimescaleDB); no other service reads it. Retention and rollup policy in Telemetry Storage & Retention.telemetry:raw, and every threshold breach to the Kafka alerts topic keyed buildingId:roomId, with no reference to whichever service consumes them. Breaches are enqueued, never awaited (send_result), so a broker outage cannot stall /telemetry/ingest.PUT /thresholds/buildings/:id) for every later edit. Both are driving adapters over the same registration use case.GET /contracts, now including each metric’s available actions./metrics. See Observability.For the request lifecycle, the plugin template, the extension guide, and the API, see the Telemetry Service internals page.