Skip to main content

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]

KeyTypeDefaultDescription
hoststring"0.0.0.0"Bind address for the HTTP/WebSocket API
portinteger8080Listen port

[broker]

KeyTypeDefaultDescription
hoststring"0.0.0.0"Embedded-broker bind address (ignored when external_url is set)
portinteger1883Plain-text MQTT v3 port
v5_portinteger1884Plain-text MQTT v5 port. Set to null to disable.
tls_portintegerTLS MQTT port (requires cert_path and key_path)
cert_pathstringPath to TLS certificate file (PEM)
key_pathstringPath to TLS private key file (PEM)
external_urlstringConnect 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:

KeyTypeDescription
idstringClient ID (used as MQTT username)
passwordstringPlain-text password (hashed internally)
allow_pubarray of stringsAllowed publish topic patterns (MQTT wildcards +/# supported)
allow_subarray of stringsAllowed subscribe topic patterns

Important: The embedded broker enforces connection-level credentials but does not enforce per-topic ACL. allow_pub/allow_sub are metadata only. For strict topic ACL, deploy against an external Mosquitto broker — see the broker deployment guide. hc-cli broker generate-mosquitto-config converts these patterns to a Mosquitto ACL file automatically.

[storage]

KeyTypeDefaultDescription
state_db_pathstring"data/state.redb"Path to the redb state database (devices, rules, users, dashboards, audit, battery latches). Relative to HOMECORE_HOME.
history_db_pathstring"data/history.db"Path to the SQLite time-series history DB.

[profiles]

KeyTypeDefaultDescription
dirstring"config/profiles"Directory of ecosystem profile TOML files (Tasmota, Shelly, Zigbee2MQTT, etc.) consumed by the topic mapper.

[rules]

KeyTypeDefaultDescription
dirstring"rules"Directory of .ron rule files. Hot-reloaded on change.

[auth]

KeyTypeDefaultDescription
jwt_secretstringunsetDeprecated. Inline HS256 secret. If set, overrides jwt_secret_file and emits a warning. Prefer the file-managed default.
jwt_secret_filestring<parent-of-state_db_path>/jwt_secretPath to a 0600 file holding the persistent JWT secret. Auto-generated on first startup so issued tokens survive restarts.
token_expiry_hoursinteger24Access JWT lifetime in hours.
refresh_token_expiry_daysinteger30Refresh token lifetime. Each /auth/refresh rotates the token; reuse triggers full chain revocation.
audit_retention_daysinteger365How many days of audit history to keep. A background task prunes older entries every 6 hours.
whitelistarray 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):

KeyTypeDefaultDescription
enabledboolfalseListen on the admin UDS.
pathstring"/run/homecore/admin.sock"Socket path.
groupstring"homecore-admin"POSIX group that owns the socket. Its members can connect.
modestring (octal)"0660"Filesystem permissions on the socket.
allowed_uidsarray of integers[]Extra UIDs allowed to connect. The process UID is always allowed.
KeyTypeDefaultDescription
initial_admin_password_filestring<parent-of-state_db_path>/INITIAL_ADMIN_PASSWORDWhere 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.

KeyTypeDescription
latitudefloatDecimal degrees, e.g. 38.9072
longitudefloatDecimal degrees, e.g. -77.0369
timezonestringIANA 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.

KeyTypeDefaultDescription
threshold_pctfloat20.0Battery percentage at or below which the latch engages.
recover_band_pctfloat5.0Recovery band added to threshold to clear the latch. Recovery fires at threshold_pct + recover_band_pct.
notify_channelstringunsetOptional hc-notify channel name. When set, the watcher sends a built-in notification on each low edge — no rule required.
notify_on_recoveredboolfalseWhen 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]

KeyTypeDefaultDescription
catchup_window_minutesinteger15On restart, fire missed time-based triggers that occurred within this many minutes. Set 0 to disable.

[startup]

KeyTypeDefaultDescription
plugin_ready_delay_secsinteger10Grace period before mode manager publishes initial states. Prevents command-before-subscribe race with plugins.

[shutdown]

KeyTypeDefaultDescription
drain_timeout_secsinteger10Seconds to wait for in-flight rule action tasks to finish on graceful shutdown before force-stopping.

[logging]

Top-level keys:

KeyTypeDefaultDescription
levelstring"info"Global log level (error, warn, info, debug, trace).
targetstable of string→string{}Per-target overrides keyed by Rust module path with underscores (e.g. hc_core = "debug"). Equivalent to RUST_LOG directives.
time_displaystring"local"Timestamp display: "local" or "utc".

[logging.stderr]:

KeyTypeDefaultDescription
enabledbooltrueEmit to stderr.
formatstring"pretty""pretty", "compact", or "json".
ansibooltrueANSI color codes (set false when piping to systemd journal).

[logging.file] — rolling file output:

KeyTypeDefaultDescription
enabledbooltrueEnable rolling file output.
dirstring"logs"Log directory (created if missing).
prefixstring"homecore"Filename prefix; rotated files become <prefix>.YYYY-MM-DD.
rotationstring"daily""daily", "hourly", "weekly", or "never".
max_size_mbinteger100Max size before rotation. Combined with rotation ("whichever first"); 0 disables size-based rotation.
compressbooltrueGzip rotated files (background thread).
prune_after_daysinteger0Delete rotated files older than N days. 0 = never prune.
formatstring"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:

KeyTypeDefaultDescription
enabledbooltrueEnable the streaming endpoint.
ring_buffer_sizeinteger500Recent log lines retained for new subscribers.

[logging.syslog] — RFC 5424 syslog forwarding:

KeyTypeDefaultDescription
enabledboolfalseEnable syslog output.
transportstring"udp""udp" (recommended) or "tcp".
hoststring"127.0.0.1"Syslog server.
portinteger514Syslog 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]

KeyTypeDefaultDescription
enabledbooleanfalseServe a directory of static files from core's root path, via tower-http ServeDir.
dist_pathstringThe 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]

KeyTypeDefaultDescription
dirstring"config/calendars"Directory of .ics calendar files. Hot-reloaded on file changes.
expansion_daysinteger400How 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.

KeyTypeDescription
idstringPlugin ID (matches the plugin's plugin_id config)
binarystringPath to the plugin binary (relative to HOMECORE_HOME)
configstringPath to the plugin config file (relative to HOMECORE_HOME)
enabledbooleanWhether the plugin should be started automatically

[metrics]

KeyTypeDefaultDescription
whitelistarray 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]

KeyTypeDefaultDescription
urlstringURL (or local path / file://) of the signed index.json.
public_keystringBase64 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.

KeyTypeDefaultDescription
enabledboolfalseTurn the exporter on.
urlstringInfluxDB base URL; writes go to <url>/api/v2/write.
tokenstringAPI token with write access to the bucket.
org / bucketstringInflux organisation and bucket.
include_devicesarray of strings[]Device-id patterns to export. Opt-in — empty exports nothing. ["*"] exports everything.
exclude_attributesarray of strings[]Attributes to drop (e.g. last_seen).
export_boolsbooltrueWrite boolean attributes as 0/1.
flush_interval_secsinteger10Flush even when the batch is not full.
batch_sizeinteger1000Maximum points per write.
channel_capacityinteger10000Bounded 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_night is true from sunset to sunrise, false during daylight hours
  • Offsets shift the transition point: on_at_offset = -30 turns night mode on 30 minutes before sunset
  • Mode state is republished via MQTT as DeviceStateChanged at 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.