Architecture Deep-Dive
This page is for contributors who want to change the engine rather than just author a profile. For the high-level picture, start with Architecture.
The registry seam
Section titled “The registry seam”The core of the “inverter is data” design is a small in-memory profile registry
(packages/inverter-core/src/registry.ts) — a Map<string, InverterProfile>. Profiles get
into it two ways:
- Code profiles (first-party npm packages) can self-register via a side-effecting import
that calls
registerProfile()and may carry a realsimulatefunction and closures. None ship in the core today — profiles are authored with@sunreye/profile-sdkand downloaded. - Data profiles (downloaded) are read from the database, validated, hydrated into
the same
InverterProfileshape, and registered.
Everything downstream — the driver, manifest, capabilities, transports — consumes
InverterProfile and never knows whether a profile originated as code or data.
Data vs. runtime profile shapes
Section titled “Data vs. runtime profile shapes”ProfileData(profile-data.ts) is the serializable artifact: the download file and theinstalled_profilesDB row. Its metrics use a declarativecomputeExprinstead of code.InverterProfile(types.ts) is the runtime shape used by the engine.hydrateProfile()compiles eachcomputeExprinto a real(values) => numberclosure, producing a single internal representation.
The role catalog (roles.ts) derives CanonicalRole
(type CanonicalRole = keyof typeof ROLE_CATALOG) so the type and the runtime data can never
drift. Capability derivation (capabilities.ts) turns the set of present roles into the
InverterCapabilities the UI renders from, and buildManifest() produces the render-ready
view sent to the browser and the API.
Two-phase, async boot
Section titled “Two-phase, async boot”Profile resolution is asynchronous and runs in initProfiles() before any routes, manifest,
or MQTT topics are built:
- Any code profiles self-register (import side effects) — none in the core today.
loadInstalledProfiles()reads everyinstalled_profilesrow, re-validates it (a stored row may predate a schema change), hydrates and registers it; an invalid row is logged and skipped so one bad download can’t take the server down.- The active profile id is resolved from the
activeProfilesetting (seeded fromINVERTER_PROFILEon first boot), and the engine is built for it.
Because the active profile shapes the REST routes, manifest, and MQTT topics once at boot, switching it is restart-scoped by design — see Distributing Profiles.
The entity model & generated surfaces
Section titled “The entity model & generated surfaces”Entities are transport-neutral: each metric yields a constraint (bounds, enum, writable) via
entityConstraint(). Every transport generates from these:
- The REST
/api/v1sub-app folds over the writable metrics to emit one validatedPUTroute per writable entity, captured into the chain so they appear in the OpenAPI spec. - The MQTT bridge builds state/command/availability topics per metric and derives Home Assistant discovery component types from the same constraints.
- The dashboard resolves every widget by role from the manifest.
Adding a metric extends all three with no route/topic/UI code.
Storage
Section titled “Storage”Telemetry is stored narrow — one row per metric per tick, keyed by inverterId and
metric key — in a TimescaleDB hypertable (packages/db). Continuous aggregates provide
per-minute / hourly / daily rollups; retention and compression policies keep raw data
bounded while preserving long-range trends. A new inverter needs no migration because
nothing is vendor-columned.
Runtime settings (inverter connection, MQTT, tariff, profile sources, active profile) live
in an app_settings table as JSONB with per-key Zod schemas, hot-reloaded on write (except
the active profile, which is restart-scoped).
Internal dashboard API
Section titled “Internal dashboard API”Separate from the stable /api/v1 surface, the dashboard uses /api/* routes (session
auth; mutations admin-only): the manifest (/api/profile), the live WebSocket
(/ws/metrics), history and rollup endpoints, cost/energy, and the settings CRUD +
test/status endpoints that back the Settings screen. These are internal —
integrations should use /api/v1.
Packages at a glance
Section titled “Packages at a glance”See Architecture → Packages for the full list.