Distributing Profiles
Profiles can be downloaded and installed at runtime from git-hosted repositories — no
redeploy, no .env edits, no code execution. Because a profile is
pure data, the only thing that ever gets fetched and stored is a
validated JSON blob.
Repo wire format
Section titled “Repo wire format”A “repo” is a public git repository containing:
-
A root
index.jsonlisting the profiles it offers:{"profiles": [{"id": "acme-hybrid","name": "Acme Hybrid","manufacturer": "Acme","version": "1.0.0","path": "profiles/acme.json","description": "Acme 5–12 kW hybrid range"}]} -
One committed
ProfileDataJSON file per entry, at thepathgiven in the index — exactly whatdefineProfileemits, serialized.
One repo can offer many profiles — hundreds is fine. The index lists every profile and
each has its own profiles/<id>.json, so a single repo can cover a manufacturer’s whole
line-up. Authoring a family is the tidy way
to do this: defineFamily emits the base plus one profile per model, and profile build
writes all of them into the same repo.
You don’t have to write this layout by hand: bunx profile build ./src/profiles.ts --out .
generates it from code-defined
profiles — validate, emit, commit, push.
The official source ships as a default, protected source — the core itself carries no
profile. https://github.com/SunReye/SunReye-Official-Profiles is merged in automatically;
it can be disabled but not removed (shown as “Default” in the UI). Add more repositories
yourself. Source URLs must be https:// (a trailing .git is optional).
Managing sources
Section titled “Managing sources”From Settings → Profiles (admin only) you can:
- Add / remove / enable repositories — the “Profile repositories” section. Edits auto-save.
- Browse all enabled repos to see available profiles, grouped by manufacturer and then family, each row labelled with its source repo and annotated installed, update available (the repo offers a semver-newer release), or downloadable.
- Download a profile — SunReye fetches its
ProfileData, validates it, and stores it. - Set active — choose which installed profile the server runs.
- Remove an installed profile (not the active one).
Update checking
Section titled “Update checking”A background checker runs independently of the poll loop: shortly after boot, then every
few hours, it syncs enabled sources and diffs installed versions against them
(semver-aware — only a genuinely newer release counts). The result is cached behind a
public GET /api/profiles/updates endpoint, so Settings → Profiles
can show an updates banner with one-click updates without an admin manually browsing. The
checker stops on graceful shutdown.
An update only reflects a newer version when the author bumped it — see
change-aware versioning for how profile build
does that automatically.
What happens on the server
Section titled “What happens on the server”- Repos are shallow-cloned into a temp cache (
clone --depth 1, or fetch + reset on update). The clone cache is disposable; the database is the source of truth. - Downloaded profiles are re-validated with the strict schema and stored in the
installed_profilestable (id,source,version,data,installedAt). - Security guards: https-only URLs, a 30 s git timeout with terminal prompts disabled, path-traversal protection, and a 1 MB per-file cap. Since profiles are data-only, the worst a malicious repo can do is fail validation.
Activation requires a restart
Section titled “Activation requires a restart”Installing a profile just persists a validated row. The active profile shapes the REST routes, the manifest, and the MQTT topics — and those are built once at boot. So:
- Downloading a profile makes it available immediately.
- Switching the active profile takes effect on the next restart, and the Settings UI shows a persistent “Restart required” banner until then.
This is intentional — it keeps the boot model simple and avoids rebuilding every surface live.
Boot flow
Section titled “Boot flow”On startup, profiles are registered in two phases before any routes or topics are built:
- Code profiles — first-party packages can self-register on import, but none ship in the core today (the box carries no profile).
- Installed profiles are read from the database, each re-validated (a stored row may predate a schema change) and registered; an invalid row is logged and skipped, so one bad download can’t take the server down.
Then the active profile id — from settings, or seeded from INVERTER_PROFILE — is resolved
and the engine is built for it. When nothing is configured (a fresh install), the server
boots in a degraded, onboarding-only mode: the admin picks a profile and tests the Modbus
connection with it from the first-run flow, then restarts into the full API.
Non-goals
Section titled “Non-goals”- No hot-reload of the active profile (restart-to-apply, above).
- No executable code in downloaded profiles — simulators and arbitrary
computeclosures remain the preserve of trusted first-party npm packages. - Not a general package manager — a repo is just a git repo with an index and JSON files.