Configuration Reference
HomeCore is configured with a single TOML file. By default it looks for config/homecore.toml relative to the base directory (current working directory, or --home / HOMECORE_HOME).
# Config resolution order:
# 1. --config /path/to/file
# 2. HOMECORE_CONFIG env var
# 3. {base_dir}/config/homecore.toml
Full annotated example
# ── Server ────────────────────────────────────────────────────────────────────
[server]
host = "0.0.0.0"
port = 8080
# ── MQTT Broker ───────────────────────────────────────────────────────────────
[broker]
host = "0.0.0.0"
port = 1883
# Optional TLS listener (runs alongside plain-text port)
# tls_port = 8883
# cert_path = "/etc/homecore/broker.crt"
# key_path = "/etc/homecore/broker.key"
# Use an external broker (e.g. Mosquitto) instead of the embedded rumqttd.
# When set, HomeCore connects as a client and skips its own listener bind.
# Required for topic-level authz enforcement — see Administration → Broker
# for the rumqttd-vs-Mosquitto split and `hc-cli broker generate-mosquitto-config`.
# external_url = "mqtt://mosquitto.local:1883"
# When [[broker.clients]] entries are present, the embedded broker requires
# credentials on CONNECT. The `allow_pub` / `allow_sub` patterns are
# metadata-only on rumqttd — they are NOT enforced at publish/subscribe time.
# For per-topic enforcement, deploy with external Mosquitto and run
# `hc-cli broker generate-mosquitto-config` to convert these patterns into
# Mosquitto's ACL file.
[[broker.clients]]
id = "internal.core"
password = "a-strong-random-password"
allow_pub = ["homecore/#"]
allow_sub = ["homecore/#"]
[[broker.clients]]
id = "plugin.hue"
password = "hue-plugin-password"
allow_pub = ["homecore/devices/hue_+/state", "homecore/plugins/hue/+"]
allow_sub = ["homecore/devices/hue_+/cmd"]
# ── Authentication ────────────────────────────────────────────────────────────
[auth]
# Nothing here is required. The HS256 signing secret is generated once and
# persisted to <state-db-parent>/jwt_secret (0600), so tokens survive
# restarts with no configuration at all. Setting `jwt_secret` inline still
# works, takes precedence, and warns at startup — it is deprecated.
token_expiry_hours = 24 # access token lifetime
refresh_token_expiry_days = 30 # refresh token lifetime
audit_retention_days = 365
# IPs that get FULL ADMIN with no token at all. Deprecated. List individual
# addresses — a CIDR range hands unauthenticated admin to every device on
# that subnet, including whatever joins it next.
# whitelist = ["127.0.0.1/32"]
# The replacement: an admin-only Unix socket for same-host tooling (hc-cli),
# authorised by filesystem permissions rather than network identity.
# [auth.admin_uds]
# enabled = true
# path = "/run/homecore/admin.sock"
# group = "homecore-admin"
# mode = "0660"
# ── Location (required for solar triggers) ───────────────────────────────────
[location]
latitude = 38.9072 # Washington D.C. defaults
longitude = -77.0369
timezone = "America/New_York"
# ── Storage ───────────────────────────────────────────────────────────────────
# Paths are relative to base_dir unless absolute.
[storage]
state_db_path = "data/state.redb" # device registry, rules, users, scenes
history_db_path = "data/history.db" # time-series attribute history
# ── Rules ─────────────────────────────────────────────────────────────────────
[rules]
# Directory containing rule RON files. Hot-reloaded on any file change.
dir = "rules"
# ── Scheduler ─────────────────────────────────────────────────────────────────
[scheduler]
# On restart, fire time-based triggers that occurred within this window.
# Set 0 to disable catch-up entirely.
catchup_window_minutes = 15
# ── Startup ───────────────────────────────────────────────────────────────────
[startup]
# Seconds to wait after startup before mode manager publishes initial modes.
# Gives plugins time to connect and subscribe before receiving cmd messages.
plugin_ready_delay_secs = 10
# ── Modes ─────────────────────────────────────────────────────────────────────
# Not a section here. Modes are defined in `config/modes.toml` next to this
# file — a fixed path, hot-reloaded on change. See the modes.toml reference
# further down.
# ── Calendars ─────────────────────────────────────────────────────────────────
# [calendars]
# dir = "config/calendars" # directory of .ics files; hot-reloaded
# expansion_days = 400 # how far ahead to expand recurring events
# ── Notifications ─────────────────────────────────────────────────────────────
[notify]
[[notify.channels]]
name = "telegram"
type = "telegram"
bot_token = "123456789:ABCDEFxxxxxxxxxxxxxxxxxxxxxxx"
chat_id = "-1001234567890"
[[notify.channels]]
name = "pushover"
type = "pushover"
api_key = "your-pushover-app-key"
user_key = "your-pushover-user-key"
[[notify.channels]]
name = "email-alerts"
type = "email"
from = "homecore@yourdomain.com"
to = ["you@yourdomain.com"]
[notify.channels.smtp]
host = "smtp.yourdomain.com"
port = 587
username = "homecore@yourdomain.com"
password = "smtp-password"
starttls = true
# ── Logging ───────────────────────────────────────────────────────────────────
[logging]
level = "info"
time_display = "local" # "local" | "utc"
[logging.targets]
hc_core = "info"
hc_api = "info"
hc_broker = "warn"
hc_mqtt_client = "info"
[logging.stderr]
enabled = true
format = "pretty" # "pretty" | "compact" | "json"
ansi = true
[logging.file]
enabled = false
dir = "logs"
prefix = "homecore"
rotation = "daily" # "daily" | "hourly" | "weekly" | "never"
max_size_mb = 100
compress = true
format = "json"
prune_after_days = 30 # delete rotated files older than N days; 0 = never prune
# A second log file dedicated to rule-engine output, so the rule trace
# does not have to be grepped out of everything else.
[logging.rules_file]
enabled = false
dir = "logs"
prefix = "rules"
rotation = "daily"
prune_after_days = 30
# Per-plugin tracing logs are forwarded to the broker on
# `homecore/plugins/<id>/logs` and merged into core's /logs/stream. The
# level is set per plugin at runtime — Plugins → the plugin → log level,
# or PATCH /api/v1/plugins/{id} — not in this file.
# [logging.syslog]
# enabled = false
# transport = "udp" # "udp" | "tcp"
# host = "192.168.1.100"
# port = 514
# protocol = "rfc5424" # "rfc5424" | "rfc3164"
# facility = "daemon"
# app_name = "homecore"
# ── Web Admin UI ──────────────────────────────────────────────────────────────
# Optional static-file mount: serves a pre-built UI bundle from core's root
# path. Off by default and left off in the standard deployment, where hc-web's
# own nginx serves the app. Originally built for the retired Leptos client.
[web_admin]
enabled = false
# dist_path = "ui/dist" # bundle directory, relative to the home dir
# ── Shutdown ──────────────────────────────────────────────────────────────────
[shutdown]
drain_timeout_secs = 10 # wait this long for in-flight rule actions, then stop
# ── Plugins (managed) ────────────────────────────────────────────────────────
# Each [[plugins]] entry defines a managed plugin that HomeCore supervises.
# [[plugins]]
# id = "plugin.hue"
# binary = "plugins/hc-hue/bin/hc-hue" # relative to HOMECORE_HOME
# config = "plugins/hc-hue/config/config.toml"
# enabled = true
# [[plugins]]
# id = "plugin.wled"
# binary = "plugins/hc-wled/bin/hc-wled"
# config = "plugins/hc-wled/config/config.toml"
# enabled = true
# ── Ecosystem profiles ────────────────────────────────────────────────────────
# Topic maps for devices that speak their own dialect (Tasmota, Shelly,
# Zigbee2MQTT). Reference profiles ship in config/profiles/examples/.
[profiles]
dir = "config/profiles"
# ── Prometheus metrics ────────────────────────────────────────────────────────
# GET /api/v1/metrics is gated by source IP, and the list is empty by default,
# which means every caller gets 403. Unlike [auth].whitelist this grants
# nothing but the metrics text.
[metrics]
whitelist = ["127.0.0.1/32"]
# ── Plugin registry ───────────────────────────────────────────────────────────
# Both fields must be set for browse + install to work; otherwise those
# endpoints return 503.
[registry]
url = "https://homecore.io/registry/index.json"
public_key = "d64zXOo99GE+uYAPbtbIwLUJHtBaACJB73cUppj93I8="
# ── InfluxDB v2 export (optional) ─────────────────────────────────────────────
# Streams numeric and boolean device attributes to InfluxDB as line protocol,
# one measurement per attribute. include_devices is OPT-IN by design: an empty
# list exports nothing. ["*"] exports everything, which with chatty sensors is
# a lot — the channel is bounded and drops oldest rather than back-pressuring
# the event bus.
# [influx]
# enabled = true
# url = "http://10.0.10.200:8086"
# token = "REPLACE_WITH_INFLUX_API_TOKEN"
# org = "homecore"
# bucket = "devices"
# flush_interval_secs = 10
# batch_size = 1000
# channel_capacity = 10000
# include_devices = ["sensor.*", "thermostat.*"]
# exclude_attributes = ["last_seen", "uptime"]
# export_bools = true
Section reference
[server]
| Key | Type | Default | Description |
|---|---|---|---|
host | string | "0.0.0.0" | Bind address for the HTTP/WebSocket API |
port | integer | 8080 | Listen port |
[broker]
| Key | Type | Default | Description |
|---|---|---|---|
host | string | "0.0.0.0" | Embedded-broker bind address (ignored when external_url is set) |
port | integer | 1883 | Plain-text MQTT v3 port |
v5_port | integer | 1884 | Plain-text MQTT v5 port. Set to null to disable. |
tls_port | integer | — | TLS MQTT port (requires cert_path and key_path) |
cert_path | string | — | Path to TLS certificate file (PEM) |
key_path | string | — | Path to TLS private key file (PEM) |
external_url | string | — | Connect to an external broker (e.g. mqtt://mosquitto:1883) instead of running the embedded one. Required for per-topic ACL enforcement; see Administration → Broker. |
[[broker.clients]] entries:
| Key | Type | Description |
|---|---|---|
id | string | Client ID (used as MQTT username) |
password | string | Plain-text password (hashed internally) |
allow_pub | array of strings | Allowed publish topic patterns (MQTT wildcards +/# supported) |
allow_sub | array of strings | Allowed subscribe topic patterns |
Important: The embedded broker enforces connection-level credentials but does not enforce per-topic ACL.
allow_pub/allow_subare metadata only. For strict topic ACL, deploy against an external Mosquitto broker — see the broker deployment guide.hc-cli broker generate-mosquitto-configconverts these patterns to a Mosquitto ACL file automatically.
[storage]
| Key | Type | Default | Description |
|---|---|---|---|
state_db_path | string | "data/state.redb" | Path to the redb state database (devices, rules, users, dashboards, audit, battery latches). Relative to HOMECORE_HOME. |
history_db_path | string | "data/history.db" | Path to the SQLite time-series history DB. |
[profiles]
| Key | Type | Default | Description |
|---|---|---|---|
dir | string | "config/profiles" | Directory of ecosystem profile TOML files (Tasmota, Shelly, Zigbee2MQTT, etc.) consumed by the topic mapper. |
[rules]
| Key | Type | Default | Description |
|---|---|---|---|
dir | string | "rules" | Directory of .ron rule files. Hot-reloaded on change. |
[auth]
| Key | Type | Default | Description |
|---|---|---|---|
jwt_secret | string | unset | Deprecated. Inline HS256 secret. If set, overrides jwt_secret_file and emits a warning. Prefer the file-managed default. |
jwt_secret_file | string | <parent-of-state_db_path>/jwt_secret | Path to a 0600 file holding the persistent JWT secret. Auto-generated on first startup so issued tokens survive restarts. |
token_expiry_hours | integer | 24 | Access JWT lifetime in hours. |
refresh_token_expiry_days | integer | 30 | Refresh token lifetime. Each /auth/refresh rotates the token; reuse triggers full chain revocation. |
audit_retention_days | integer | 365 | How many days of audit history to keep. A background task prunes older entries every 6 hours. |
whitelist | array of strings | [] | Deprecated. IP addresses or CIDR ranges that bypass JWT auth and receive Admin access. Both IPv4 and IPv6 supported (e.g. ["127.0.0.1/32", "::1/128"]). Prefer [auth.admin_uds] for same-host tooling. |
[auth.admin_uds] — Unix domain socket listener for same-host admin tooling (replaces whitelist):
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Listen on the admin UDS. |
path | string | "/run/homecore/admin.sock" | Socket path. |
group | string | "homecore-admin" | POSIX group that owns the socket. Its members can connect. |
mode | string (octal) | "0660" | Filesystem permissions on the socket. |
allowed_uids | array of integers | [] | Extra UIDs allowed to connect. The process UID is always allowed. |
| Key | Type | Default | Description |
|---|---|---|---|
initial_admin_password_file | string | <parent-of-state_db_path>/INITIAL_ADMIN_PASSWORD | Where the first-boot admin password is written, 0600. Set to "" to skip the file and only log it. Never rewritten after first boot. |
[location]
Required for SunEvent and SunEvent offset triggers.
| Key | Type | Description |
|---|---|---|
latitude | float | Decimal degrees, e.g. 38.9072 |
longitude | float | Decimal degrees, e.g. -77.0369 |
timezone | string | IANA timezone name, e.g. "America/New_York" |
[battery]
Drives the battery alert watcher. The watcher synthesizes
device_battery_low and device_battery_recovered events from device
state changes, with hysteresis enforced in core (latches at
threshold_pct, clears at threshold_pct + recover_band_pct). See
Battery monitoring for the full picture.
| Key | Type | Default | Description |
|---|---|---|---|
threshold_pct | float | 20.0 | Battery percentage at or below which the latch engages. |
recover_band_pct | float | 5.0 | Recovery band added to threshold to clear the latch. Recovery fires at threshold_pct + recover_band_pct. |
notify_channel | string | unset | Optional hc-notify channel name. When set, the watcher sends a built-in notification on each low edge — no rule required. |
notify_on_recovered | bool | false | When true and notify_channel is set, recovery edges also notify. |
[battery]
threshold_pct = 20.0
recover_band_pct = 5.0
# notify_channel = "all"
# notify_on_recovered = false
[scheduler]
| Key | Type | Default | Description |
|---|---|---|---|
catchup_window_minutes | integer | 15 | On restart, fire missed time-based triggers that occurred within this many minutes. Set 0 to disable. |
[startup]
| Key | Type | Default | Description |
|---|---|---|---|
plugin_ready_delay_secs | integer | 10 | Grace period before mode manager publishes initial states. Prevents command-before-subscribe race with plugins. |
[shutdown]
| Key | Type | Default | Description |
|---|---|---|---|
drain_timeout_secs | integer | 10 | Seconds to wait for in-flight rule action tasks to finish on graceful shutdown before force-stopping. |
[logging]
Top-level keys:
| Key | Type | Default | Description |
|---|---|---|---|
level | string | "info" | Global log level (error, warn, info, debug, trace). |
targets | table of string→string | {} | Per-target overrides keyed by Rust module path with underscores (e.g. hc_core = "debug"). Equivalent to RUST_LOG directives. |
time_display | string | "local" | Timestamp display: "local" or "utc". |
[logging.stderr]:
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Emit to stderr. |
format | string | "pretty" | "pretty", "compact", or "json". |
ansi | bool | true | ANSI color codes (set false when piping to systemd journal). |
[logging.file] — rolling file output:
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable rolling file output. |
dir | string | "logs" | Log directory (created if missing). |
prefix | string | "homecore" | Filename prefix; rotated files become <prefix>.YYYY-MM-DD. |
rotation | string | "daily" | "daily", "hourly", "weekly", or "never". |
max_size_mb | integer | 100 | Max size before rotation. Combined with rotation ("whichever first"); 0 disables size-based rotation. |
compress | bool | true | Gzip rotated files (background thread). |
prune_after_days | integer | 0 | Delete rotated files older than N days. 0 = never prune. |
format | string | "json" | "json", "compact", or "pretty". |
[logging.rules_file] — separate rule-engine log capturing only hc_core at debug regardless of the global level. Disabled by default. Same field set as [logging.file] plus its own defaults (prefix "rules", format "pretty").
[logging.stream] — live /api/v1/logs/stream WebSocket:
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable the streaming endpoint. |
ring_buffer_size | integer | 500 | Recent log lines retained for new subscribers. |
[logging.syslog] — RFC 5424 syslog forwarding:
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable syslog output. |
transport | string | "udp" | "udp" (recommended) or "tcp". |
host | string | "127.0.0.1" | Syslog server. |
port | integer | 514 | Syslog port. |
[notify]
Channels are declared as a list under [[notify.channels]]. The name is referenced from rule Notify actions. The reserved name "all" fans out to every registered channel.
[[notify.channels]]
name = "primary_email"
type = "email"
smtp_host = "smtp.example.com"
smtp_port = 587
username = "automation@example.com"
password = "..."
from = "automation@example.com"
to = ["alerts@example.com"]
[[notify.channels]]
name = "phone_push"
type = "pushover"
api_token = "..."
user_key = "..."
[[notify.channels]]
name = "telegram"
type = "telegram"
bot_token = "..."
chat_id = "..."
Built-in providers: email (SMTP), pushover, telegram. Channels that fail to initialise at startup are logged and skipped — they don't block the rest of the system.
[web_admin]
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Serve a directory of static files from core's root path, via tower-http ServeDir. |
dist_path | string | — | The bundle directory, relative to HOMECORE_HOME. |
When enabled, core serves that bundle at /, with API routes under /api/v1
taking priority and a SPA fallback returning index.html for anything
unmatched.
It is off by default and the standard deployment leaves it off: the web UI is
hc-web, which ships its own nginx image and proxies
/api/v1 to core. This section was built for the Leptos client core used to
bake in, and survives as a generic static mount for anyone self-hosting a
bundle.
[calendars]
| Key | Type | Default | Description |
|---|---|---|---|
dir | string | "config/calendars" | Directory of .ics calendar files. Hot-reloaded on file changes. |
expansion_days | integer | 400 | How many days forward to expand recurring events into individual occurrences. |
Glue devices
The path to glue device definitions is fixed at <base>/config/glue.toml and is not configurable from homecore.toml. See Virtual / glue devices for the file format (timers, switches, counters, modes).
[[plugins]]
Each entry declares a plugin for homeCore to supervise: spawn it, watch its heartbeat, start/stop/restart it, and push its configuration.
You rarely write one. A registry install records itself in
config/plugins/managed.toml, which homeCore owns and rewrites; your
homecore.toml is never touched. The effective plugin set at boot is
static entries ∪ managed records − uninstalled ids, with managed records
winning on an id collision — so uninstalling a statically-declared plugin
sticks (a tombstone) without homeCore editing your file to do it.
Write a [[plugins]] block for a plugin you built or unpacked yourself and
want supervised from a path you control.
| Key | Type | Description |
|---|---|---|
id | string | Plugin ID (matches the plugin's plugin_id config) |
binary | string | Path to the plugin binary (relative to HOMECORE_HOME) |
config | string | Path to the plugin config file (relative to HOMECORE_HOME) |
enabled | boolean | Whether the plugin should be started automatically |
[metrics]
| Key | Type | Default | Description |
|---|---|---|---|
whitelist | array of strings | [] | Source IPs or CIDR ranges allowed to scrape GET /api/v1/metrics. Empty — the default — means every caller gets 403. IPv4 and IPv6. |
Opening this stays an explicit act: metrics expose device, rule, and plugin
counts, and defaulting them open to whatever subnet the host happens to sit on
would widen the attack surface silently. The 403 body names the exact line to
add. See Metrics.
[registry]
| Key | Type | Default | Description |
|---|---|---|---|
url | string | — | URL (or local path / file://) of the signed index.json. |
public_key | string | — | Base64 ed25519 public key that signs the index. |
Both must be set, or /registry/plugins and /plugins/install return
503. Core verifies the index signature and each artifact's SHA-256 before
unpacking anything. See Plugins.
[influx]
Optional InfluxDB v2 export of device state. Off unless enabled = true.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Turn the exporter on. |
url | string | — | InfluxDB base URL; writes go to <url>/api/v2/write. |
token | string | — | API token with write access to the bucket. |
org / bucket | string | — | Influx organisation and bucket. |
include_devices | array of strings | [] | Device-id patterns to export. Opt-in — empty exports nothing. ["*"] exports everything. |
exclude_attributes | array of strings | [] | Attributes to drop (e.g. last_seen). |
export_bools | bool | true | Write boolean attributes as 0/1. |
flush_interval_secs | integer | 10 | Flush even when the batch is not full. |
batch_size | integer | 1000 | Maximum points per write. |
channel_capacity | integer | 10000 | Bounded backlog. Full means oldest events are dropped rather than back-pressuring the event bus. |
One measurement per attribute, tagged with device_id, area, plugin_id,
and device_type.
modes.toml reference
The modes configuration is a separate file (config/modes.toml), hot-reloaded when changed.
# Solar modes — computed from sunrise/sunset with optional offset
[[modes]]
name = "mode_night"
type = "solar"
# on_at_offset = 0 # minutes after sunset (negative = before)
# off_at_offset = 0 # minutes after sunrise (negative = before)
# Manual boolean modes — toggled via API or rule actions
[[modes]]
name = "mode_away"
type = "manual"
default = false
[[modes]]
name = "mode_vacation"
type = "manual"
default = false
[[modes]]
name = "mode_movie"
type = "manual"
default = false
Solar mode behavior:
mode_nightistruefrom sunset to sunrise,falseduring daylight hours- Offsets shift the transition point:
on_at_offset = -30turns night mode on 30 minutes before sunset - Mode state is republished via MQTT as
DeviceStateChangedat every transition
Mode API:
# Get all mode states
curl -s http://localhost:8080/api/v1/modes -H "Authorization: Bearer $TOKEN" | jq
# Set a manual mode on
curl -s -X PATCH http://localhost:8080/api/v1/modes/mode_away \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"on": true}'
# Adjust solar offset (minutes relative to sunset/sunrise)
curl -s -X PATCH http://localhost:8080/api/v1/modes/mode_night \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"on_at_offset": -30}'
Environment variable overrides
Some sensitive values can be set via environment variables instead of the config file:
HOMECORE_JWT_SECRET="your-secret" \
HOMECORE_LAT="38.9072" \
HOMECORE_LON="-77.0369" \
HOMECORE_TZ="America/New_York" \
./bin/homecore
These are particularly useful in Docker deployments where secrets should not be baked into config files.