Hearth M2 + M3 capability

Hearth · the privacy-first ANPR appliance

Your gate sees.
No one else does.

Hearth is a small, opinionated ANPR appliance for the driveway, the loading bay, the back gate. It runs on a single machine on your LAN, stores everything locally in SQLite, signs every privileged action with Ed25519, and never phones home. Recently added: Home Assistant auto-discovery, signed OTA updates, eight-language UI, opt-in VLM fallback, and intra-operator mesh networking — 202 tests passing in 6.2 s.

On-prem
SQLite + WAL
LAN-only · no telemetry · no analytics pixels
Ed25519
Audit chain
One signed line per privileged action
NaCl
SecretBox backups
Argon2id-derived keys · one passphrase
Per-cam
PII discipline
Retention + optional plate-hashing after N hours
+5
Capabilities
HA · OTA · i18n · VLM · mesh
202
Tests
+49 recent · pass in 6.2 s

01 — Privacy by construction

The right defaults, by default.

Most ANPR systems start with the camera and let policy catch up. Hearth inverts that — retention, hashing, and audit are first-class, not aftermarket bolts. The PRD mandates them; the schema enforces them; the CLI surfaces them. You can't accidentally keep a plate longer than you intended.

Scope

LAN-only by default.

The FastAPI dashboard binds to 127.0.0.1; the storage path is local. There is no built-in cloud sync to accidentally enable, no telemetry, no analytics pixel.

/about/network · live socket list · default deny
Retention

Per-camera windows.

Each camera carries its own retention_days. The daily purge respects them; retention_days=0 means never for explicit allow-listing. No accidental forever-keep.

store.add_camera(retention_days=30)
PII

Hash before you forget.

Optionally replace plate text with sha256:hex after N hours — keep analytics, drop the raw plate well before the full purge fires.

run_purge(store, hash_after_hours=24)
Snapshots

Sharded, swept.

JPEG snapshots are sharded by date; the purge sweeps orphans as part of the daily run. Nothing lingers in deleted-but-referenced limbo.

snapshots/YYYY/MM/DD/*.jpg · daily sweep

02 — The audit chain

One signed line per privileged action.

Every retention purge, every user creation, every rule mutation, every backup, every login attempt — written as a single row whose signature covers the prior row's hash plus the canonical payload. You can prove what happened, when, and by whom — and you can do it offline.

Schema, in one breath

  • prev_hash — SHA-256 of the previous row's signing input.
  • hash — SHA-256 of prev_hash plus the canonical payload.
  • signature — Ed25519 over hash, when a keypair is configured.
  • actor / action / target — the human-readable trail.
  • payload_json / signed_at — every detail you need at 2 a.m.

Verify offline with the public verify key. Export a full bundle for archival; replay-verify it in one pass with verify_chain().

from hearth.audit import (
    AuditKeypair, record, list_audit,
    verify, export_chain, verify_chain,
)

kp = AuditKeypair.from_file("audit_key.json")

# Per-row verify.
for row in list_audit(store):
    if row.signature:
        assert verify(row, kp.verify_key_b64)

# Bundle verify (offline replay).
bundle = export_chain(store, kp)
ok, total = verify_chain(bundle)
# ok == total → chain has not been tampered with.

03 — Authentication & access control

Argon2id passwords. RFC 6238 TOTP. Four roles.

The password-hashing parameters are OWASP 2024 defaults — time-cost 3, memory-cost 64 MiB, parallelism 4. Two-factor authentication is standard RFC 6238 TOTP with a ±1 step tolerance window. The role model is small enough to memorise: admin, operator, viewer, service.

Password

Argon2id, modern parameters.

Constant-time verification. OWASP 2024 cost defaults, configurable per environment without changing APIs.

hash_password(pw) → verify_password(pw, hashed)
2FA

TOTP for every role.

20-byte base32 secrets. The provisioning URI works in Aegis, 1Password, Authy, and every standard authenticator.

generate_totp_secret() · totp_uri() · verify_totp()
RBAC

Four roles.

Admin is wildcard. Operator can ack, manage groups, override. Viewer reads. Service hits the ingest + emit surface only.

can(role, action) → bool
Users

Create from the CLI.

One command, optionally with TOTP. Storage is Argon2id-hashed; secrets are encrypted at rest with the operator key.

hearth user create --role admin --password …

04 — The rule engine

Conditions in, side-effects out.

Rules are pure data: a condition dict (min_confidence, region, camera_scope, time_window) plus a trigger (webhook, mqtt, gpio) and a target string. The engine evaluates them on every persisted event and dispatches matches through the registered dispatcher classes.

Conditions

  • min_confidence — drop low-confidence reads before firing.
  • region — only fire for matching plate regions.
  • camera_scope — restrict to specific cameras (int or list).
  • time_window — local-clock HH:MM-HH:MM windows.
  • group_id — plate must be in the named group.

Triggers

  • WebhookDispatcher — HMAC-signed POST. Per-request timeout.
  • MQTTDispatcher — local broker fan-out. Topic per rule.
  • GPIODispatcher — Raspberry Pi only. Sysfs or mock backend.
from hearth.rules import create_rule, RuleEngine, TriggerType
from hearth.triggers.webhook import WebhookDispatcher

create_rule(
    store,
    trigger_type=TriggerType.WEBHOOK,
    target="https://example.com/hooks/anpr",
    conditions={
        "min_confidence": 0.9,
        "region":         "us",
        "time_window":    "07:00-19:00",
    },
    group_id=4,
    enabled=True,
)

engine = RuleEngine(
    store,
    dispatchers={
        TriggerType.WEBHOOK: WebhookDispatcher(
            timeout=5.0,
            hmac_secret="change-me-quarterly",
        ),
    },
)

05 — Retention & backup

The right things stay. The wrong things can't.

Hearth's retention model has two knobs: when a row gets hashed (its plate text becomes sha256:hex) and when it gets deleted. Backups are encrypted tar.gz archives with a magic prefix, Argon2id-derived keys, and NaCl SecretBox — restorable on a fresh host with one command.

Purge

Daily cron, per-camera.

Each camera owns its retention_days. The purge ignores cameras at zero, deletes rows past their window, and cleans orphaned snapshots.

run_purge(store, hash_after_hours=24)
Hash

Drop PII, keep counts.

Plate text becomes sha256:hex on a configurable delay — your weekly report still shows trends; the raw plate is unrecoverable.

hash_after_hours=24 · sha256:hex
Backup

One archive, full restore.

NaCl SecretBox over an Argon2id-derived key, magic prefix hrth, manifest.json with version + window.

hearth backup OUT --passphrase … --days 7
Restore

Fresh host, same chain.

The audit keypair travels in the archive. Verify the restored chain offline — same signatures, same hashes.

hearth restore IN --target DB --passphrase …

06 — Dashboard & CLI

Run it from the terminal. Watch it from the browser.

The dashboard is FastAPI + HTMX — server-rendered, no SPA build, runs locally on a Raspberry Pi or a mini-PC. Seven views: dashboard, events, plates, groups, rules, audit, settings. The CLI mirrors every privileged action.

CLI subcommands

  • init — create HEARTH_HOME, mint the audit keypair.
  • serve — FastAPI + HTMX dashboard on 127.0.0.1.
  • ingest IMAGE — run an image through the pipeline.
  • purge — retention sweep + optional plate hashing.
  • backup / restore — encrypted archive round-trip.
  • audit list / export — read or bundle the chain.
  • user create / list — RBAC + optional TOTP.
  • version — print Hearth's version.
# 1. Initialise + first admin.
hearth init
hearth user create --email ops@example.com \
                   --role admin --password '…'

# 2. Register a camera.
hearth ingest /tmp/front-gate.jpg --camera 1

# 3. Run the operator console.
hearth serve --host 127.0.0.1 --port 8765

# 4. Nightly cron.
hearth purge --hash-after-hours 24
hearth backup ~/backups/$(date -u +%F).tar.gz \
              --passphrase "$BACKUP_PASSPHRASE" --days 7

# 5. Verify the chain (any time, anywhere).
hearth audit export > chain.json

06b — Capability horizon · M2 + M3 pulled forward

Five new modules. One operator, one consent posture.

PRD-01 originally roadmapped Home Assistant integration and OTA updates for M2, an 8-language UI for M3, and VLM fallback + inter-appliance mesh for Post-1.0. All five ship with real Protocol architecture and deterministic stub adapters today; production integrations (WireGuard, Tailscale, Gemini Flash-Lite, S3 release channels) plug in behind the same Protocols without changing the appliance contract. +49 tests; 202 pass in 6.2 s.

01 · HA Discovery · M2

For Home Assistant power-users & integrators

Home Assistant auto-discovers the appliance.

hearth.ha_discovery publishes retained MQTT discovery topics so HA auto-discovers Hearth as a device + per-group count sensors + gate binary sensor + per-camera Camera entities. Builds on the existing hearth.triggers.mqtt — no new broker dependency.

hearth.ha_discovery · MQTT retained topics
02 · OTA · M2

For field-deployed appliances behind firewalls

OTA — Ed25519-signed bundles only.

hearth.ota. UpdateChannel Protocol with slots for HTTPS / Mender / S3. Every bundle Ed25519-signed by ICYCASTLE's release key; appliance refuses unsigned bundles. Default policy: announce, never auto-install — the operator authorises every install.

hearth.ota · UpdateChannel · signed bundles
03 · i18n · M3

For Indian HOAs, societies and SMBs

Eight UI languages.

hearth.i18n. en / hi / mr / ta / te / bn / gu / kn covering UI labels, gate-event names, WhatsApp alert templates, and audit-log entries. translate_detailed() reports fallback-to-English semantics so missing translations never silently degrade the operator UX.

hearth.i18n · translate_detailed
04 · VLM fallback · Post-1.0

For occluded / dirty / worn-out plates

VLM hard-case fallback — opt-in, budget-capped.

hearth.vlm_fallback. Opt-in VLM second-opinion for occluded / dirty plates. Per-day FallbackBudget cost-cap counter. Every fallback call is audit-chained so the operator has a record of which crops were sent to a cloud model.

hearth.vlm_fallback · FallbackBudget
05 · Mesh · Post-1.0

For multi-gate societies & multi-driveway SMBs

Inter-appliance private mesh.

hearth.mesh. Intra-operator only — society with multiple gates, SMB with multiple driveways. WireGuard / Tailscale slots behind a MeshTransport Protocol. Explicitly not cross-organisation federation (that's Eyrie's job).

hearth.mesh · MeshTransport (WireGuard / Tailscale)

07 — Roadmap

The path from M0 to GA.

M0 ships today — 153 tests, the full feature surface described on this page. M1 wires PlateKit's RTSPSource into the pipeline so cameras stream live. M2 hardens the installer for non-developers. M3 is GA with paid support tiers and a hardware reference design.

— Stage · M0 · shipped

Prototype

Storage, audit, auth, rules, retention, backup, dashboard, CLI. 153 tests green. The full surface described on this page is here, today.

— Stage · M1

Alpha

Live RTSP ingest via PlateKit, ONVIF discovery helpers, per-rule rate-limit defaults, schema migrations from M0.

— Stage · M2

Beta

Single-binary installer for Pi 5 + mini-PCs, signed OTA updates, multi-camera dwell-time rules, operator-friendly setup wizard.

— Stage · M3 · GA

v1.0

SemVer commitment, hardware reference designs, paid support SKU, multilingual dashboard, EFF-reviewed privacy posture.

08 — Stated commercial policy

Three lines we won't cross.

Stated commercial policy, written into the EULA. If you need any of these, we are not the right tool — and we'll point you to someone who is.

— refusal · 01

No federated hotlist by default

Your plate reads stay on your appliance. There is no opt-in cloud sync, no "neighbourhood" mode, no LE-pipeline integration. We will not build one in v1, v2 or v3.

— refusal · 02

No facial recognition

Hearth reads plates. Not faces. Not gait, not "behavioural anomalies." When we ship a face capability — if we ever do — it will be off by default and require a written policy file.

— refusal · 03

No silent telemetry, ever

We will never add an outbound socket the user didn't authorise on the network page. This commitment is in the EULA, not just the marketing.

09 — Hearth

Put the hearth where it belongs.
On your gate. On your hardware. On your network.

EFF-reviewed privacy posture. SQLite + WAL. Ed25519 audit chain. LAN-only operation. Argon2id + TOTP. NaCl SecretBox backups. Sleep well.