/

Developer Guide

Contracts Service

Implementation reference for the contracts-service. For its architectural role, see Contracts Service Architecture; for the domain model, see Telemetry Distribution.

The only Rust service in the platform. Built on Axum and Tokio, it runs an HTTP API and a background telemetry tunnel in a single process, holding per-building preferences in memory for a lock-free hot path.


Source Layout

The source is organised by role: a composition root and shared kernel at the crate root, the two inbound adapters (api/ for HTTP, tunnel.rs for the broker stream), and outbound integrations plus cross-cutting infrastructure grouped under infra/.

PathResponsibility
src/main.rsComposition root: connect Mongo, hydrate the in-memory map, start the tunnel, serve the Axum router.
src/state.rsAppState: the shared DashMap projection plus the typed Mongo collection handle.
src/models.rsPreferenceDocument, MetricContract, and the discovery response types.
src/api/ (dashboard.rs, data.rs, init.rs)Inbound HTTP handlers: the aggregated catalog (GET /), preferences CRUD, and defaults seeding.
src/tunnel.rsInbound stream worker: routes telemetry:raw to each event’s own building, and records fan-out metrics.
src/infra/db.rsMongo access: connect (with unique index), load_all (hydration), upsert_preference.
src/infra/discovery.rsResolves downstream metric endpoints from *_METRICS_URL environment variables.
src/infra/metrics.rsPrometheus registry, telemetry counters and fan-out latency, and the GET /metrics handler.
graph TD
    MAIN["main.rs\ncomposition root"] --> API["api/\ndashboard.rs, data.rs, init.rs"]
    MAIN --> TUNNEL["tunnel.rs\nbackground worker"]
    MAIN --> DB["infra/db.rs\nconnect + hydrate"]
    API --> STATE["state.rs\nAppState: DashMap + Mongo handle"]
    TUNNEL --> STATE
    API --> DISC["infra/discovery.rs"]
    API --> METRICS["infra/metrics.rs"]
    TUNNEL --> METRICS
    STATE -.-> DB

Configuration

VariableDefaultPurpose
MONGO_URImongodb://localhost:27017Durable store for preferences (database crowdvision, collection preferences).
REDIS_URLredis://127.0.0.1/Broker for the telemetry tunnel.
PORT3000HTTP listen port.
*_METRICS_URLAny variable with this suffix is treated as a service base URL for metric discovery.

State and Data Model

pub struct AppState {
    pub building_preferences: Arc<DashMap<String, Vec<String>>>, // hot path
    pub mongo_col: Collection<PreferenceDocument>,               // durable
}

pub struct PreferenceDocument {
    pub id: Option<ObjectId>,
    pub building_id: String,      // unique index
    pub allowed_columns: Vec<String>,
}

DashMap shards its internal locks across cores, so the tunnel’s millions of reads almost never contend with an occasional write. The Mongo collection enforces one document per building via a unique index on building_id.


Startup Sequence

1. connect(MONGO_URI)          -> ensure unique index on building_id
2. load_all()                  -> seed the DashMap from persisted documents
3. start_telemetry_tunnel()    -> spawn the background filter task
4. axum::serve(router)         -> begin handling HTTP

Hydration at boot means a restart restores all preferences before the first request is served.


Persistence Model (Hot Path / Cold Path)

Reads and the tunnel operate purely in memory; durability is handled off the request path.

Eventual durability

Because the database write is fire-and-forget, a crash in the narrow window between the in-memory insert and the persisted upsert can lose the most recent change. This is an accepted trade-off for hot-path latency; the next successful save reconciles it.


HTTP API

All routes except /health and /metrics require the mesh-verified x-gateway-claims header (decoded by a GatewayClaims extractor that returns 401 on a missing or malformed header) — the same contract every other service consumes. The user-facing routes are gated at the edge (require_gateway_auth in the Caddyfile, the /contracts AuthorizationPolicy rule in Kubernetes); POST /preferences/init is an internal call and receives the building creator’s claims forwarded by twin-service. This enforces authentication only; per-building authorization is deferred to the Cedar phase.

Method · PathDescription
GET /preferences/{building_id}Return { building_id, allowed_columns }. 404 if the building has no entry. Requires x-gateway-claims.
POST /preferences/{building_id}Replace the allowed-column list (body { allowed_columns }). Instant cache write + async persist. Returns { status, building_id, active_columns }. Requires x-gateway-claims.
POST /preferences/init/{building_id}Idempotently seed defaults ["roomName", "roomMaxOccupancy"] with an atomic or-insert, so a repeat call is a safe no-op. Called by the twin-service on building creation, forwarding the creator’s x-gateway-claims.
GET /The aggregated metric catalog (see Metric Discovery). Requires x-gateway-claims.
GET /metricsPrometheus exposition: telemetry events received/published counters and fan-out latency. Open (scraped in-cluster).

The Telemetry Tunnel

Started at boot, the tunnel opens a dedicated Redis PubSub connection and subscribes to telemetry:raw.

sequenceDiagram
    participant R as Redis (telemetry:raw)
    participant T as Tunnel
    participant D as DashMap
    participant F as Redis (telemetry:filtered:{id})
    R-->>T: { type, buildingId, roomId, value, ... }
    Note over T: spawn a task per message (ingestion never blocks)
    T->>D: is buildingId a known building?
    alt known
        T->>F: PUBLISH telemetry:filtered:{building_id} (full payload)
    end

Metric Discovery

GET / builds the catalog the frontend uses to populate column pickers.

  1. discover_services() collects every environment value whose key ends in _METRICS_URL.
  2. Each service’s GET /contracts is fetched and deserialised (either a ServiceMetricsContract or a bare metric list).
  3. Each metric is tagged with its source_service and added via push_unique_metric, which deduplicates on (metric_key, interface_name, source_service).

New metric types therefore appear automatically once a service advertises them; no change is needed here.

Known gap: deletion cleanup

There is no DELETE /preferences/{building_id}. When a building is removed from the twin-service, its entry remains in both the DashMap and Mongo, and the tunnel keeps evaluating it. A deletion endpoint (called from the twin on removal) is the intended fix.