Skip to content

Backend Architecture

For contributors working on the Pankha Fan Control server. The backend is a Node.js + TypeScript Express app with a WebSocket hub and PostgreSQL, living in the backend/ workspace of the monorepo. For the concept-level view, read How the Server Works first.

backend/src/
├── index.ts # Express app, router mounts, service startup
├── routes/ # REST routers (one file per API area)
├── services/ # The actual logic (below)
├── database/ # Connection pool + schema.sql
├── license/ # License tiers and activation
└── config/ # fan-profiles-defaults.json (SST)

Everything interesting lives in backend/src/services/ - single-purpose classes wired together at startup:

ServiceResponsibility
AgentManagerRegistry of all agents: status, per-agent settings (in-memory maps loaded from the database), error-state persistence
AgentCommunicationAgent WebSocket protocol: registration, data frames, error frames
WebSocketHubThe central WebSocket server; subscription topics (systems:all, agents:all) for dashboard clients
DataAggregatorMerges raw agent data with that system’s settings into the shape the frontend consumes
DeltaComputerComputes changed-values-only updates per client (the ~95% bandwidth saving)
CommandDispatcherSends commands to agents (fan speed, config changes) and tracks their completion
FanProfileControllerThe control loop: every tick (default 2000ms), evaluates curves + hysteresis + stepping and dispatches speed commands
FanProfileManagerFan profile CRUD, curve points, assignments
FanProfileTypeManagerUser-defined profile categories
CalibrationServiceCalibration runs, measurement validation, health/trend data, stall tracking
DownsamplingServiceTiered history compression (1-min / 5-min / 30-min averages by age), scheduled daily
ProfileServiceBMC profile catalog for IPMI agents: scans backend/profiles/, resolves extends inheritance
UpdateDownloadServiceHub staging: downloads release binaries so agents update from your server, not the internet
graph LR
    A["Agent (WebSocket)"] --> AC[AgentCommunication]
    AC --> DA[DataAggregator]
    DA --> DC[DeltaComputer]
    DC --> Hub[WebSocketHub]
    Hub --> FE["Dashboard clients"]
    DA --> DB[("PostgreSQL")]
    FPC[FanProfileController] --> CD[CommandDispatcher]
    CD --> A

Two rules worth knowing before touching this pipeline:

  • The database is authoritative; memory is a cache. AgentManager holds settings in maps for speed, loaded from PostgreSQL on registration. Write to the database, then update the map.
  • Agent lifecycle events must broadcast to both topics (agents:all and systems:all) - the dashboard subscribes to systems:all, and events sent only to agents:all silently never reach it.

Schema lives in backend/src/database/schema.sql and auto-loads in Docker (docker-entrypoint-initdb.d). Core tables:

TablePurpose
systemsRegistered agents + their configuration
sensors / fansHardware metadata, labels, visibility, ordering
fan_profiles / fan_curve_points / fan_profile_assignmentsProfiles, their curves, and fan mappings
fan_configurationsPer-fan control settings (control sensor, limits)
sensor_group_visibilityGroup-level show/hide
monitoring_dataTime-series history (downsampled by age)
backend_settingsKey/value server settings (see API Reference for the allowed keys)
licenses / license_configLicense keys and active state
  • SST files: dropdown value ladders come from frontend/src/config/ui-options.json and default profiles from backend/src/config/fan-profiles-defaults.json - edit the SST, never hardcode values in consumers.
  • Delta discipline: any new per-system field the dashboard should react to must be added to DeltaComputer’s tracked fields and the frontend’s React.memo comparison (Development: Frontend) - miss either and the UI silently stops updating for that field.
  • Log hygiene: one log line per event or state change at INFO; per-tick chatter belongs at DEBUG/TRACE.
Terminal window
npm install # once, at the repo root (npm workspaces)
npm run dev:backend # ts-node with auto-restart on :3000
npm run typecheck

See Building from Source for the full environment and API Reference for the REST/WebSocket surface.