Architecture
A modular monolith with plug-in edges.
The core knows no vendor. Vendor knowledge lives only in importers and connectors, behind two small interfaces. One edge binary covers a laptop, a gateway next to a controller network, and later a hosted deployment. This page is the design; the status of each part is on the roadmap.
Shape
packages/ core · rules · templates · alarms · histories · api-schema · mcp · sdk (pure, no I/O, no vendor names)
importers/ niagara-station · jci-caf · distech-gfx · metasys-export · schedule-sheet (file in, facts out, offline)
connectors/ bacnet-ip · modbus · niagara (edge side) · … (live in, observations out, read-only)
apps/ edge (SQLite, API, MCP, web, plug-in host) · web · cli · hosted (later)
vendor-modules/ niagara-module (separate toolchain, optional, gated by legal review)Rules enforced in CI: packages/* may not import from importers or connectors; the core never names a vendor type. Equipment view templates are keyed on semantic types (VAV, VAV with reheat, AHU, FCU), never on a vendor's class names. Alarm evaluation and history collection are edge services on model points; vendor alarm and history configuration is imported as a proposal.
Two interfaces
Importer
import(bytes, options) → {
artifact, // sha256, size, format
assertions[], // entities, relationships,
// bindings, config proposals
bindingHints[], // native addresses found
questions[],
coverage, // typed / attached / unknown
redactions[], // what was replaced, never the value
}Pure. No sockets, no second file, no database. Runs in a worker with memory and time limits.
Connector
describe() → { capabilities: {
discover, read, subscribe,
history, alarms, write: false } }
open(config) → session
session.discover(scope)
session.read(refs) · subscribe(refs)
session.history(ref, range, cursor)
session.alarms(cursor) · health()Every reading carries its source. Cursors make histories and alarms resumable. Writing is a separate interface no early connector implements.
The semantic core
Every fact is an assertion with provenance: observed, imported, inferred (rule, inputs, confidence), confirmed or rejected (person, time, what it supersedes). Nothing is overwritten. A snapshot is an immutable, hashed set of assertions plus the artifact hashes it came from; diff is a core operation: same persisted identity first, then same name under a matched parent. Secrets found in artifacts become typed placeholders and are never stored. Questions are first-class. Entities: site, building, floor, space, zone, equipment, point, device, network, program. Relationship names align with Brick and Haystack so export to either is a formatter, not a dependency.
Deployment and trust boundaries
| Boundary | What crosses it | Controls |
|---|---|---|
| Controllers, devices → edge | Discovery, reads, subscriptions, histories | Read-only by type; no write service in the default build; explicit scopes. |
| Vendor station → edge (optional) | Station export: structure, histories, alarm records, live values | Outbound from the station; documented services first; a module only after legal review; no write path. |
| Browser → edge | API, MCP over HTTP, static assets | Local accounts, roles (viewer, technician, admin), CSRF, strict CSP, works with no WAN. |
| Edge → hosted (optional) | Snapshots, deltas, a remote-access tunnel | Off by default; admin enables; outbound only; the enable screen names destination and data classes. |
| Tenant ↔ tenant (hosted) | Nothing | Row-level security on the session tenant, per-tenant keys, cross-tenant tests. |
Technology, and why
| Decision | Choice | Why |
|---|---|---|
| Core language | TypeScript on Node LTS | One language from the browser walkthrough to the edge to a hosted job; a large pool of contributors; MCP's reference SDK is TypeScript-first. Written from scratch against synthetic fixtures. |
| Edge persistence | SQLite, one file per site | No daemon, backs up with a copy, fine for one building's model and histories. Postgres for hosted behind the same repository interface. |
| API | Hono with zod schemas and generated OpenAPI, served offline | Small, standards-based, one set of types for UI, API and MCP. |
| Web app | React, all assets bundled | Nothing fetched from a CDN at runtime. |
| BACnet/IP | A TypeScript BACnet stack, read services only in the default build | Vendor-neutral and license-clean; runs in the edge process. |
| Modbus | A small TCP and RTU library; register maps from CSV; serial in a child process | Mature and isolated. |
| Semantic vocabulary | Own schema with Haystack- and Brick-aligned names | Start with the tags and relationships the importers can populate; export to either standard is a formatter. |
| Vendor station module | Java, signed, separate repository | The only thing that runs inside some stations; optional; publication gated by legal review. |
Deliberately absent: a message broker, a time-series database, Kubernetes, a plugin marketplace, and any language model inside the edge. Histories go into SQLite tables with monthly partitions; the repository interface is the seam if a site outgrows that. AI integration is MCP only, pointed at whatever model the operator chooses, including none.
Decision records
Architecture decisions are recorded in the repository under docs/adr/ as they are made: the vendor-neutral core, the importer and connector split, read-only by type, the license split, the signing custody rule, and the first-connector choice. Repository.