Halite is a FastAPI backend paired with a React 19 SPA, shipped as one Docker image. The backend serves the compiled SPA viaDocumentation Index
Fetch the complete documentation index at: https://www.halite-app.com/llms.txt
Use this file to discover all available pages before exploring further.
HALITE_STATIC_DIR; there is no separate web server.
Layers at a glance
Two kinds of settings
Halite has two separate settings systems that serve different purposes:Settings | AppSettings | |
|---|---|---|
| Source | Environment variables | Single DB row |
| File | backend/src/halite/config.py | backend/src/halite/settings/models.py |
| Covers | Database URL, cookie secret, listen host/port, log level | Salt-API URL, eauth, username, encrypted password, poller intervals |
| Editable | At deploy time | At runtime via the Settings UI |
| Read | On process start | On RuntimeConfig.boot() and RuntimeConfig.reload() |
Settings) are read-only at runtime. Changing them requires redeploying or restarting the process.
App settings (AppSettings) are the single row in the app_settings table. The salt password is encrypted at rest via settings/crypto.py. When you save new credentials through the UI, the settings route writes the row and calls runtime.reload(db) — no restart needed.
RuntimeConfig — the hub
RuntimeConfig (runtime.py) is the central coordination object. It owns exactly one SaltAPIClient, all background schedulers, and — when the event stream is enabled — the EventHub and event consumer.
At boot, the lifespan handler calls RuntimeConfig.boot(db), which:
- Reads the
AppSettingsrow. - Decrypts the salt password.
- If all three of
salt_api_url,salt_api_username, and password are present, creates aSaltAPIClientand callslogin()to verify the credentials immediately. - Starts whichever schedulers have non-zero intervals configured.
- If
event_stream_enabledis set, creates theEventHuband starts theEventStreamConsumer.
runtime.salt is None and no schedulers or consumer start. Every salt-backed endpoint degrades gracefully (503 or empty data) rather than crashing.
On a settings change, runtime.reload(db) tears down all running schedulers, the event consumer, and the existing client atomically, then rebuilds them from the updated row.
RuntimeConfig manages are:
MinionStateScheduler— syncs key status, presence, and grains intominion_snapshotsFleetIngestScheduler— builds the fleet compliance pictureInventoryScheduler— populates the inventory indexJobsIndexScheduler— upserts recent jobs intojobs_index
EventStreamConsumer (activity/consumer.py) is not a polling scheduler but a long-lived task that streams salt-api’s /events bus, persisting events to activity_events and publishing them to the in-process EventHub for live SSE delivery. See How Halite Works for the full flow.
App entry point
main.py::create_app is a factory function — there is intentionally no module-level app object. Importing the module must never read environment variables (that would break tests), so Settings() is only constructed inside the factory.
Uvicorn launches it with the --factory flag:
init_engine(settings.database_url)— creates the SQLAlchemy async engine.seed_builtin_roles(s)— ensures the three built-in roles (admin,operator,viewer) exist.bootstrap_admin(s)— creates the defaultadmin/changemeuser (flaggedmust_change_pw=True) when no users exist yet. The credentials are hardcoded inbootstrap.py; theBOOTSTRAP_ADMIN_*lines in.env.exampleare stale and not read.RuntimeConfig.boot(db)— wires up the salt client and schedulers.
settings.static_dir is set (i.e., HALITE_STATIC_DIR is configured), the SPA build is mounted. A catch-all route returns index.html for client-side routes; real static files (favicon, touch icon, robots.txt) are served first so browsers get the correct content type.
Related pages
- How Halite Works — the poll-into-DB pattern and graceful degradation
- Backend — feature modules, SaltAPIClient, and RBAC