Data model¶
This page synthesizes the entity/domain model across the three registry/telemetry services of the trustmint tier: ThingHub (the canonical thing registry), ThingIO (telemetry
scopes + pipelines), and ThingLab (device simulation). It is the cross-service map an integrator needs to reason about where a piece of state lives.
What’s live vs parked. ThingHub is the always-on registry. The device telemetry pipeline (EMQX MQTT ingress →
thingio-consumer→ TimescaleDB) is BUILT and wired but PARKED (DIAL_ONLY=true) — the ThingIO entities and query surface below are defined and stable, but live end-to-end telemetry consumption is gated. ThingLab runs api-only (THINGLAB_ROLE=api); its telemetry emitter role is parked. The live data plane is EMQX + TimescaleDB + Temporal + MSK (Kafka).
Service split at a glance¶
Service |
Language |
Owns |
Live? |
|---|---|---|---|
ThingHub |
Go |
Thing registry, Registrar, Assignment, Repository, Scope (identity), ThingModel |
live |
ThingIO |
Go |
Scope (identity + attribute), Pipeline, TelemetryRecord, ConsumerApp |
telemetry pipeline parked ( |
ThingLab |
Python (FastAPI, api-only) |
Template, Fleet, Run |
api live; emitter parked |
All three delegate authorization to the DotID policy-engine (see ADR-0004) and authenticate via Keycloak (OIDC/JWT). Domain events flow over MSK (Kafka). Persistent state is PostgreSQL; telemetry time-series is TimescaleDB.
ThingHub — the thing registry¶
ThingHub is the universal thing registry and single source of truth for device identity, metadata, and lifecycle state. Other services reference ThingHub as the canonical device registry.
Thing¶
The core entity. A thing represents any device or asset — a robot, a sensor, a warehouse product, or any physical item that needs tracking.
Thing
├── thing_id (nullable; null at enrollment, NanoID 16 chars assigned at provisioning)
├── serial_number (optional, immutable once set; burned in factory)
├── device_key (symmetric key registered by manufacturer; used for symmetric_key provisioning)
├── parent_thing_id (nullable, for sub-device → gateway)
├── registrar_id (registrar that created it — permanent, immutable)
├── access_code (nullable; generated at provisioning; registrar can regenerate)
├── name (display name)
├── status (lifecycle state: ENROLLED → PROVISIONED → ACTIVE → ...)
└── attributes (dynamic key-value pairs, JSONB)
Identity fields vs attributes. Identity fields (thing_id, serial_number,
device_key, parent_thing_id, registrar_id, access_code) are structural — indexed,
constrained, used for lookups. Attributes are descriptive key-value pairs (JSONB) —
flexible, user-defined, and used for filtering and OTA target selection.
serial_number is optional because not every thing has one at enrollment: factory devices
have SN burned in; cloud-first SKU registrations may not; warehouse inventory and virtual
things never have one.
Device descriptor. Devices manufactured in-house are flashed with a read-only descriptor
(analogous to USB VID/PID): vid (vendor), pid (product/model), hw_revision. The
descriptor fields are attributes, not identity fields — they describe a class of device
(many units share a vid:pid). At first boot the device reports them as attributes.
ThingModel¶
ThingModel
├── id
├── registrar_id (registrar that owns this model)
├── name (display name, e.g. "BROIT-X1")
├── properties (JSONB — includes provisioning_method and provisioning_config)
├── created_at
└── updated_at
ThingAssignment¶
The account’s reference into a thing — the row that materializes the repository (see ADR-0001).
ThingAssignment
├── thing_id
├── account_id (account with access)
├── org_id (optional)
├── assigned_at
└── ended_at (null = active)
Key design decisions:
account_id/org_idare attributes, not schema-level foreign keys — things are scoped and filtered by attributes.Registrar is the account that created the thing: permanent, immutable, always has full access, can manage assignments and regenerate the
access_code.Assigned accounts are N accounts with concurrent access granted via
access_code.There is no owner / claim / transfer concept. Access is additive and managed through assignments. See thing access model.
Registrar & Repository¶
Registrar (PENDING → ACTIVE | REJECTED) and the per-account Repository (1:1 with account,
materialized by the repositories table) are covered in
Registrar/ThingMake/Arsenal and
ADR-0003. Group and tag
memberships key off the assignment row, not the thing PK — see
ADR-0002.
Scope (ThingHub)¶
A scope is a per-account saved dynamic filter over the account’s assigned things,
filtering identity fields only (type, model_id, status, registrar_id). Scopes are
private (not shareable, even within an org) and re-evaluate live. Attribute-based filtering is
ThingIO’s job (below).
Provisioning validator¶
When a device enrolls via DDI with a proof field, ThingHub/thingmake dispatches the proof
to a provisioning validator selected by the thing model’s provisioning_method. Five
methods:
Method |
Proof |
How it works |
|---|---|---|
|
none |
Provisioned immediately on DDI enrollment; no verification. Low-security / dev. |
|
|
Validator SHA-256-hashes the key and compares against the stored |
|
|
X.509 chain validation against the manufacturer’s trusted CA from |
|
|
Validator calls the manufacturer’s HTTPS webhook; expects |
|
n/a |
DDI provisioning disabled; provision only via the management API. |
Provisioning method is per-model, stored in ThingModel.properties JSONB. On success:
thing_id (NanoID) assigned, status → PROVISIONED, access_code generated, X.509 cert +
license token issued. On failure: 403 Forbidden, thing stays ENROLLED.
Lifecycle: ENROLLED → PROVISIONED → ACTIVE → SUSPENDED → DECOMMISSIONED. (This is the
ThingHub registry lifecycle; the device-facing wire lifecycle is the five-state
state machine.)
Domain events (MSK / Kafka)¶
ThingHub publishes: thinghub.thing.enrolled, thinghub.thing.provisioned,
thinghub.thing.state-changed, thinghub.thing.updated, thinghub.thing.decommissioned.
ThingIO and ThingLab consume these to maintain local device references.
ThingIO — telemetry, scopes, pipelines¶
ThingIO provides tenant-scoped device grouping (Scopes), telemetry storage/query, configurable stream-processing pipelines, and a consumer-app registry. Four tenant-scoped core entities:
Scope (ThingIO)¶
Like ThingHub scopes but richer: ThingIO scopes filter by identity fields (via ThingHub)
and device attributes (location, firmware_version, fleet, region, manufacturer,
…). Attribute filters support trailing-* prefix wildcards.
{
"device_types": ["cleaning_robot", "sensor"],
"attributes": {
"location": "warehouse-*",
"firmware_version": "2.*",
"fleet": "production"
}
}
Evaluation: ThingIO fetches the identity-filtered thing list from ThingHub, resolves each thing’s attributes, applies the attribute filters (with wildcards), returns the matched set.
Fields: id, tenant_id, name, description, filter (JSON), thing_count, created_by
(user|app|system), app_id (optional ConsumerApp link), timestamps.
Pipeline¶
A Pipeline is a stream-processing job over device telemetry. Three-tier model:
Tier 1 — Managed: auto-provisioned per tenant on first device enrollment; not tenant-editable. Templates:
device_health_rollup,telemetry_aggregation,anomaly_baseline.Tier 2 — App: created by tenants from configurable templates (
threshold_alert,geo_fence,rolling_average); tenant-controlled lifecycle.Tier 3 — Platform: reserved for platform-level pipelines.
Fields: id, tenant_id, name, description, tier, status
(running|stopped|error|provisioning), template, params (JSON), scope_id,
app_id, input_topics, output_topics, timestamps.
Pipelines are part of the parked telemetry pipeline (
DIAL_ONLY). The entity + API are defined; live stream execution is gated until the consumer is dialled on.
TelemetryRecord¶
An individual telemetry data point, stored in TimescaleDB (hypertable time-series).
Fields: id, tenant_id, thing_id, key (metric name), value (serialized), ts
(epoch-ms), ingested_at.
ConsumerApp¶
An external OAuth2 application registered to consume ThingIO data.
Fields: id, tenant_id, name, description, client_id (globally unique),
scopes_count, pipelines_count, permissions (JSON array), timestamps.
Telemetry data plane (when dialled on)¶
Devices publish over EMQX (MQTT 5.0, mTLS on 8883; see the
DDI-1 contract §4). thingio-consumer ingests from the MQTT
ingress and writes TelemetryRecords into TimescaleDB; domain/analytics events flow over MSK;
Temporal orchestrates longer-running workflows. Query is exposed as paginated telemetry
queries (filters on thing, scope, key, time range) at console.flexgalaxy.ai/thingio/.
ThingLab — device simulation (api-only)¶
ThingLab lets developers create virtual device fleets, define sensor/software profiles, and
execute emulation runs producing realistic telemetry — end-to-end IoT workflow testing
without physical hardware. It runs api-only today: the FastAPI management surface
(THINGLAB_ROLE=api) is live; the telemetry emitter role (THINGLAB_ROLE=emitter) is
parked pending its device-SDK + emulator dependency, which now lives in the separate
SiriusVoyager/ddi repo.
Template¶
A reusable device archetype: base thing type (maps to a ThingHub type), sensor profiles (type, data range, sampling rate, noise model), software profile (firmware version, capabilities, OTA compatibility), connectivity (protocol + behavior), failure modes.
Fleet¶
A group of virtual devices for a test scenario: template reference, fleet size (1–10,000), variation rules (firmware spread, sensor calibration offsets), optional geographic distribution, scheduling.
Run¶
A time-bounded simulation execution: fleet references, duration, scenario (e.g. OTA rollout, mass disconnect, load spike), telemetry output (to ThingIO over the same MQTT/HTTP path as real devices — emitter, parked), post-run results (messages sent, errors, latency).
Sensor profiles model realistic telemetry (type, distribution, sampling rate, noise, failure injection); software profiles simulate firmware/OTA cycles and rollback scenarios. These are configuration the api surface manages; actual telemetry emission is gated on the parked emitter role.