Architecture
Everything in SunReye derives from one thing: the active inverter profile. The profile is data — a register map plus semantic metadata — and every surface (the dashboard, the REST API, the MQTT topics, Home Assistant discovery) generates itself from it.
Inverter (Modbus TCP) ──► Core engine (Elysia) ──► TimescaleDB (time-series) │ │ ├─ WebSocket ──► Web dashboard (SvelteKit) │ │ ├─ REST /api/v1 ──► Third-party integrations └── (or simulator) │ └─ MQTT bridge ──► Home Assistant / brokers ▲ Inverter profile (data) register map + semantic metadata ──► drives every surface aboveThe flow
Section titled “The flow”- The core engine (
apps/server) polls the inverter over Modbus TCP once a second (the “God loop”). No inverter? A built-in simulator generates coherent fake metrics. - Each sample is broadcast to browsers over a WebSocket (
/ws/metrics) and persisted to TimescaleDB in narrow form — one row per metric per tick. - The manifest — a capability description built from the profile at boot — drives the SvelteKit dashboard, the auto-generated REST API, and the MQTT bridge. Nothing hard-codes vendor register keys.
Packages
Section titled “Packages”| Package | Role |
|---|---|
packages/inverter-core |
The engine: Modbus codec/driver, the profile registry, capability derivation, and transport-neutral entity descriptors (constraints, bounds, enums) that every transport generates from. |
packages/profile-sdk |
Tooling to author, validate, and score inverter profiles. Profiles live in their own repo (SunReye-Official-Profiles); the core ships none. |
apps/server |
The core engine (Elysia + Bun): poll loop, WebSocket stream, REST API, MQTT bridge, history endpoints. |
apps/web |
The SvelteKit dashboard. Builds itself from the profile manifest. |
packages/db |
Drizzle schema + TimescaleDB setup. Metrics are stored narrow, so a new inverter needs no migration. Also home to DB-backed runtime settings. |
packages/env / packages/auth / packages/config |
Shared env schema (single source of truth), auth, and tooling. |
Why narrow storage matters
Section titled “Why narrow storage matters”Metrics are stored one row per metric per tick, keyed by inverterId and metric key —
not as wide vendor-specific columns. Adding a metric, or a whole new inverter, requires
no database migration. This is the schema-level expression of “an inverter is data.”
TypeScript · Bun · Turborepo · SvelteKit 5 + Tailwind v4 · Elysia · Drizzle + PostgreSQL/TimescaleDB · Better-Auth · MQTT.
For a deeper walk through the boot sequence, the registry seam, and the entity model, see the Architecture Deep-Dive.