Skip to content

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.

Provenance: every fact in the model is observed from a live device, imported from an engineering file, inferred by a rule with a confidence, or confirmed by a person. Nothing is overwritten; confirmations supersede.OBSERVEDIMPORTEDINFERREDCONFIRMEDread from a live devicefound in an engineering fileby a rule · confidence 0.00–1.00by a named person, datedsource: connector id,device, object, timesource: file hash,locator inside the filesource: rule id, the factsit used, the scoresupersedes an earlier fact;nothing is overwrittenbelow threshold → a question for a technician
Fig. Provenance. Four labels, carried by every fact, shown on every screen, returned by every API call.

Deployment and trust boundaries

Deployment: controllers and devices on the customer network feed a local Archipelago edge read-only; a browser on the LAN uses it; an optional outbound-only connection reaches a hosted service.CUSTOMER NETWORK · NO INBOUND RULESBACnet/IP controllersVAVs, AHUs, metersModbus devicesTCP and RTUNiagara stationoptional connector, outboundread-only, no write pathARCHIPELAGO EDGEsmall Linux box · VM · laptopconnectors · read-only by typeimporters · offline, isolatedmodel · provenance on every factstorage · local, artifacts encryptedAPI + MCP · one authorization checkweb app · served from the boxworks with the WAN unpluggedOperator browseron the LAN · HTTPSno internet neededHosted (optional)remote access, multi-siteoff by default · admin enablesedge dials out · never inboundreadreadexportHTTPSwss · outbound
Fig. Deployment. Every arrow from the building side is a read. The only connection that leaves the site is optional and starts from the edge.
BoundaryWhat crosses itControls
Controllers, devices → edgeDiscovery, reads, subscriptions, historiesRead-only by type; no write service in the default build; explicit scopes.
Vendor station → edge (optional)Station export: structure, histories, alarm records, live valuesOutbound from the station; documented services first; a module only after legal review; no write path.
Browser → edgeAPI, MCP over HTTP, static assetsLocal accounts, roles (viewer, technician, admin), CSRF, strict CSP, works with no WAN.
Edge → hosted (optional)Snapshots, deltas, a remote-access tunnelOff by default; admin enables; outbound only; the enable screen names destination and data classes.
Tenant ↔ tenant (hosted)NothingRow-level security on the session tenant, per-tenant keys, cross-tenant tests.

Technology, and why

DecisionChoiceWhy
Core languageTypeScript on Node LTSOne 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 persistenceSQLite, one file per siteNo daemon, backs up with a copy, fine for one building's model and histories. Postgres for hosted behind the same repository interface.
APIHono with zod schemas and generated OpenAPI, served offlineSmall, standards-based, one set of types for UI, API and MCP.
Web appReact, all assets bundledNothing fetched from a CDN at runtime.
BACnet/IPA TypeScript BACnet stack, read services only in the default buildVendor-neutral and license-clean; runs in the edge process.
ModbusA small TCP and RTU library; register maps from CSV; serial in a child processMature and isolated.
Semantic vocabularyOwn schema with Haystack- and Brick-aligned namesStart with the tags and relationships the importers can populate; export to either standard is a formatter.
Vendor station moduleJava, signed, separate repositoryThe 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.