A modern, cloud-native IoT Application Enablement Platform built in Go and React.
DeviceChain™ connects, manages, and processes data from large, heterogeneous device fleets — covering device lifecycle, telemetry ingestion, command & control, organizational modeling, and multi-tenancy — and exposes everything through a GraphQL API.
It is a ground-up rebuild of the SiteWhere IoT platform that keeps the proven domain model while replacing the heavy Java/Spring stack with efficient, operationally simple microservices that run on any Kubernetes cluster. The goal is a self-hosted, full-featured platform that is operationally simple, architecturally complete, and unmetered all the way to production scale.
Project status: DeviceChain is pre-release and under active development. The documentation marks each capability as available, planned, or in design, and this repository is the source of truth for what currently builds and runs.
- Go-native microservices — sub-second startup, a small memory footprint, and single-binary services per functional area.
- Operator + CRDs, not shell scripts — a Kubernetes operator reconciles a
declarative
Instancecustom resource, so an instance's deployment shape is version-controllable and GitOps-friendly. - GraphQL-first API — introspectable and self-documenting; no generated client stubs and no REST surface to maintain.
- A lean, fully open-source stack — NATS JetStream is the entire messaging / MQTT / KV backbone, native JWT handles auth, TimescaleDB is the single data store, and OpenTofu provisions infrastructure. Two dependencies to run locally: NATS + TimescaleDB. No Java, Keycloak, Kafka, ZooKeeper, Redis, or Mosquitto.
- A uniform relationship model — device context is a typed relationship graph rather than rigid assignments, so new entity types compose without schema churn.
- Self-hosted and unmetered — Apache-2.0 with no open-core split and no per-device pricing. Device inventory, twin state, command delivery, multi-tenancy, high availability, and SSO are part of the open platform, not a paid tier. Run it inside your own environment with full data ownership.
DeviceChain ships under Apache License 2.0 with no open-core split. There is no proprietary "Enterprise" edition that gates production-critical capability — clustering / high availability, multi-tenant isolation, persistent command delivery, and SSO are all part of the open stack, as are roadmapped capabilities like OTA updates when they land. Nothing production-critical is reserved for a paid tier.
Every runtime dependency is OSI-approved open source (Apache 2.0, MIT, BSD, MPL 2.0), and nothing in the stack is encumbered by a source-available or business-source license. Because the platform runs entirely inside your own environment, the data — and the compliance boundary around it — stays with you: no third-party processor to vet and full source auditability.
DeviceChain favors open, widely-implemented standards over bespoke protocols, so existing tools and off-the-shelf device clients work without a special SDK:
- Device transport — devices connect over standard MQTT (the built-in NATS MQTT server on port 1883); HTTP, CoAP, and WebSocket transports are planned. Any conformant MQTT client works unchanged.
- Edge ingestion — a dedicated adapter ingests Eclipse Sparkplug B by joining your existing Sparkplug MQTT environment as a Host Application, so brownfield edge fleets stream in without changing anything on the device side. It also drives authoritative device presence — online/offline taken from the Sparkplug birth/death handshake rather than inferred from an activity timeout.
- API — GraphQL for all external APIs (one introspectable schema per service). Internal service-to-service communication is asynchronous over NATS.
- Authentication — native RS256 JSON Web Tokens (RFC 7519) with a standard JWKS endpoint and RFC 7638 key thumbprints for rotation. Device credentials are pluggable, including X.509 certificates and access tokens. user-management also runs a standards-based OAuth 2.1 authorization server (PKCE, RFC 8414 metadata, RFC 8707 audience-bound tokens) that secures MCP access.
- Enterprise SSO (optional) — an optional Dex sidecar adds OIDC / SAML / LDAP without a heavyweight identity provider per tenant.
- Observability — Prometheus metrics, and Kubernetes-standard
/healthz(liveness) and/readyz(readiness) probes on every service. - Orchestration & IaC — runs on any CNCF-conformant Kubernetes cluster (EKS, GKE, AKS, K3s, kind), packaged with Helm, with infrastructure provisioned by OpenTofu (a Terraform-compatible, Linux Foundation project).
- Data — PostgreSQL + TimescaleDB: a single SQL engine for both relational entity data and time-series events.
DeviceChain is a set of stateless Go microservices over a shared core library, coordinated by a Kubernetes operator and connected by NATS JetStream. A single instance serves all tenants (a shared-microservice model); tenant isolation is enforced at the messaging and storage layers rather than by running separate pods per tenant.
| Service | Responsibility |
|---|---|
| event-sources | Inbound device transports. Decodes raw messages and publishes them onto the pipeline. |
| sparkplug-ingest | An opt-in, stateful Eclipse Sparkplug B Host Application: connects out to per-tenant customer brokers, runs the Sparkplug session machine, maps {group}/{node}[/{device}] identities to devices, and feeds the same pipeline — the first transport to assert authoritative device presence. Leader-elected so exactly one replica connects. |
| device-management | Devices, device types + versioned device profiles, the typed relationship graph, alarm objects (level-state integration), and event resolution. |
| event-management | Persists resolved events to TimescaleDB and serves time-series queries over GraphQL. |
| user-management | Identities, per-tenant memberships, roles, and two-tier JWT issuance / validation (JWKS). |
| device-state | Live last-known-state projection per device (presence, latest location and measurements). |
| command-delivery | Persistent, two-way command dispatch to devices. |
| dashboard-management | Stores tenant dashboard definitions; the embeddable widget packages render live telemetry over them. |
| notification-management | Routes triggered alarms to humans — per-tenant policy over email (SMTP) and webhook, with per-severity escalation. |
| event-processing | The DETECT + REACT pipeline and the sole alarm engine: taps the resolved-events stream, detects conditions over a keyed-streaming CEL core, and dispatches actions (raise-alarm, send-command, and outbound connectors). Rules are authored on the profile as forms or on a visual automation canvas. |
| outbound-connectors | Delivers REACT's outbound actions to external systems — an HTTP/webhook httpCall and a publish to MQTT, Kafka, AWS SNS, and AWS SQS — over a tenant-scoped, versioned connector whose credentials live in the secret store. Isolated from the detection engine in its own process. |
| ai-inference | An opt-in service that drafts a detection rule from a natural-language description and hands it to the same compiler humans use — the AI proposes, the compiler disposes, and it never sits in the replay-correct path. Providers are operator-registered with write-only API-key handles; external-model use is per-tenant opt-in and fail-closed, and the model a tenant runs is a tiered entitlement. |
| mcp | An opt-in OAuth 2.1 Resource Server exposing read-only tools (devices, state, telemetry, alarms, commands) to AI agents over the Model Context Protocol, fronting the per-area GraphQL under the caller's own token. |
| operator | A controller-runtime operator reconciling the Instance custom resource (an instance's deployment shape). |
- NATS JetStream is the single backbone for asynchronous messaging, the MQTT ingress, and key-value caching / locking — no separate Kafka, Redis, or MQTT broker.
- TimescaleDB (PostgreSQL + the TimescaleDB extension) is the single data store for both relational entity data and time-series events. Events live in hypertables with compression and continuous aggregates.
device → MQTT/NATS → event-sources → (decoded event)
→ device-management → (resolved event: device + relationship context attached)
→ event-management → TimescaleDB
During resolution, device-management looks up the device's tracked relationships and denormalizes them onto each event as index dimensions, so downstream queries like "every reading for Building 7" resolve without joins.
Infrastructure (NATS, TimescaleDB, ingress, TLS) is provisioned by OpenTofu at cluster-creation time. The operator assumes that infrastructure exists and is responsible only for materializing DeviceChain workloads and maintaining their configuration — keeping cluster bootstrapping out of application code.
Each service loads its configuration into a typed schema and fails closed: an unknown key, a wrong type, or an invalid value is rejected at startup rather than silently ignored. Secrets a service must hold (channel credentials, connector auth) are kept in a pluggable secret store — envelope-encrypted in Postgres under an instance root key by default, with external managers (Vault, cloud KMS) as drop-in backends — and are referenced by handle rather than stored in the clear.
That root key lives in the instance's Kubernetes Secret, which means it lives in
etcd, and no database backup contains etcd. So dcctl bootstrap writes an
encrypted escrow artifact for it by default, sealed under a passphrase you
choose, and dcctl secrets escrow verify checks it still matches the running key
without needing that passphrase. Without it, a database backup restored into a new
cluster rehydrates secrets that nothing can decrypt — with no error at restore
time. See Disaster Recovery.
High availability, stated precisely. dcctl bootstrap --ha replicates both
tiers from one flag. The message broker runs as a 3-node RAFT cluster with every
JetStream stream and KV bucket replicated across it — and dcctl ha verify
asserts that from live broker state rather than from the rendered
configuration, because a three-node cluster whose every stream is single-replica
costs three times the compute, reports three healthy peers, and survives nothing.
Both databases run as replicated PostgreSQL clusters managed by an operator: the
relational store holds each write until a standby confirms it, while the event
store falls back to asynchronous replication instead, because unpersisted events
are still held durably in the messaging layer and can be replayed. Note that
--ha therefore also triples database disk, since volumes are sized per instance.
What is proven, and what is not. A 3-node instance survives one node loss, and both databases restore into a freshly built cluster. Both are drilled against a real multi-node cluster, and the two claims that can carry a negative control do: the replication check is also run against a deliberately non-HA instance and required to fail, and the restore drill is repeated with the escrowed key withheld and required to fail at the decrypt — because a check nobody has watched fail is not yet evidence. Not proven: MQTT session continuity across a broker failover, and graceful-drain behavior. And the restore story covers the two databases — JetStream stream state and object storage have not been through a restore drill.
dcctl — the platform CLI — bootstraps a complete instance with one command. It
is self-contained: the operator manifests, Helm chart, and OpenTofu config are
embedded in the binary, so no source checkout, kubectl, helm, or kustomize
is required. The single host prerequisites are Docker, kind, and
OpenTofu (the CLI's preflight checks guide you through any that are missing).
# Stand up a full instance on a local kind cluster at http://localhost/
dcctl bootstrap local --host localhost --no-tls
# …then open the printed console URL and log in with the seeded credentials.
# Tear it all back down (use --keep-cluster to uninstall only the instance)
dcctl destroyThe bootstrap pipeline renders config → tofu apply (NATS + TimescaleDB + ingress)
→ installs the CRDs/operator → helm installs the instance → seeds the initial
superuser → waits for readiness → reports the access URL — and is idempotent on
re-run: it reads back every credential the instance is already running (root key,
broker auth, service auth) and reuses it rather than minting a replacement, and
stops rather than guessing if it cannot tell whether the instance exists.
It defaults to published images; pass --build for the from-source
(ko → local registry) developer path. Building dcctl itself: cd backend/cli && make build.
Because DeviceChain is pre-release, no image tags have been published yet — use the
--buildpath until the first release is cut.
DeviceChain models the physical world with a small set of composable concepts. The defining choice is that device context is expressed as a typed relationship graph rather than a fixed assignment record:
- Device — the thing that connects and reports; an instance of a device type.
- Device Type — the taxonomy/identity layer: name, appearance (icon/colors), and classification. A type references at most one device profile.
- Device Profile — a distinct, versioned (draft / publish / rollback)
capability aggregate that owns a device class's metric, command, and
alarm-rule definitions. Many types can share one profile, and a device
resolves its capabilities through
device → type → profile. - Asset — the real-world thing a device monitors (Device / Person / Hardware).
- Area — a spatial/organizational location, optionally with polygon boundaries and zones; areas nest into hierarchies.
- Customer — an organizational owner; customers also nest into hierarchies.
- Groups — one uniform entity group collects any of the above with either
static membership (an explicit list) or dynamic membership — a CEL
selector over the members' attributes (e.g.
attr["climate"] == "arid"), resolved on read as an indexed query, so the group stays current as attributes change.
Entities are addressed uniformly by entity type + id, and connected by
typed, directed relationships. A relationship type carries a Tracked flag
that selects which relationships are denormalized onto events for indexing — so a
device can relate to many customers, areas, assets, or even other devices at once,
and context can evolve without schema migrations.
DeviceChain also distinguishes current state from history: append-only
events in TimescaleDB hypertables, versus key-value attributes in three
scopes — CLIENT (device-reported), SERVER (platform-only), and SHARED
(platform-set, device-readable; the channel for remote config and OTA). Attributes
double as classification facets: a per-tenant registry declares which attribute
keys are browse/filter axes, and those same attributes are what a dynamic group's
selector matches on — one primitive, no separate tag store.
A single shared set of microservices serves all tenants. Isolation is enforced where it matters:
- Storage — every tenant-owned row carries a
tenant_id, and a central, fail-closed database scope applies aWHERE tenant_id = …predicate to every read and stamps it on every write. A tenant-scoped query with no tenant in context is rejected. - Messaging — subjects are scoped per tenant (
{instance}.{tenant}.{suffix}). - Auth — a person is a global, email-keyed identity that holds one membership per tenant. Logging in yields an identity token (admin API); selecting a tenant exchanges it for a single-tenant tenant token that drives the data plane. The per-request tenant comes from that verified tenant token's claim; the per-message tenant is derived from the messaging subject.
Adding a tenant is a control-plane operation — the superuser creates it through the
admin API / console (an iam_tenants record), not a Kubernetes resource. Tenants
do not get their own pods.
An operator packages what a tenant gets through a first-class tenant tier — an operator-defined, sold-not-tuned entity (gold / silver / bronze, or whatever an operator names) that subsystems read but never redefine: it supplies the per-tenant governance ceilings a tenant inherits and the AI models a tenant may use. A shed dial is tuned; a tier is sold. Tiers, priority, and limits are never client-settable and never a token claim.
| Area | Choice |
|---|---|
| Backend | Go 1.26+ (Go Workspaces), net/http + graph-gophers/graphql-go, GORM |
| Frontend | TypeScript, React 19 (hooks), Vite, TailwindCSS, shadcn/ui, GraphQL Code Generator (client-preset) |
| Messaging / KV / MQTT | NATS JetStream |
| Data | PostgreSQL 17+ with TimescaleDB Community Edition |
| Auth | Native RS256 JWT + JWKS; optional Dex for OIDC/SAML/LDAP |
| Orchestration | Kubernetes (controller-runtime operator), Helm |
| Infrastructure as Code | OpenTofu |
| Observability | Prometheus, zerolog |
Minimum to run locally: a NATS server (a single ~10MB binary) and TimescaleDB (via Docker). No Java, Keycloak, Kafka, ZooKeeper, Redis, or Mosquitto.
backend/ Go monorepo (Go Workspaces)
core/ shared library — entity, auth, messaging, config, rdb, graphql
services/ microservices — device-management, user-management, event-management,
event-sources, device-state, command-delivery, dashboard-management,
notification-management, event-processing, outbound-connectors,
ai-inference, mcp
k8s/ Kubernetes operator (controller-runtime)
cli/ dcctl — instance bootstrap / destroy and admin tooling
tools/ maintainer-only Go tools (schema differ, disaster-recovery drill)
frontend/ npm workspace — apps/console (React + TypeScript management console)
+ packages (client SDK, dashboard runtime, embeddable widgets)
deploy/ Helm chart + OpenTofu modules
docs/ Docusaurus documentation site
The docs/ directory is a Docusaurus site covering concepts
(architecture, domain model, multi-tenancy), deployment, and guides. See
docs/docs/intro.md to start, or read it published at
docs.devicechain.io.
- A question, or something that confused you — Discussions. If a page or a workflow did not make sense, that is worth posting; confusion is a defect we can fix.
- An idea or feature request — Discussions → Ideas, so the design conversation happens in the open.
- Something is broken — open an issue. There are short forms for install failures, missing telemetry, docs problems, and everything else.
- A security vulnerability — do not file publicly. Email admin@devicechain.io.
DeviceChain is pre-1.0. Interfaces can still change in response to what people run into, which they cannot once 1.0 freezes them — so early, rough reports are worth more to us than polished ones later. "I got stuck here and gave up" is a perfectly good issue.
DeviceChain is licensed under the Apache License 2.0. See LICENSE and NOTICE. Copyright is held by The DeviceChain Authors.
"DeviceChain" and the DeviceChain logo are trademarks of IoT Innovations, LLC (U.S. application pending, USPTO Serial No. 99910096). The Apache 2.0 license covers the code, not the Marks — see TRADEMARK.md for the trademark policy.