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, all passing.

FIG · A plate read. No event egressed.
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 · all passing

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 Ed25519 signature covers the canonical payload of that action. You can prove what happened, when, and by whom — and you can do it offline.

Schema, in one breath

  • actor / action / target — the human-readable trail.
  • payload_json / signed_at — every detail you need at 2 a.m.
  • signature — Ed25519 over the canonical signing input, when a keypair is configured.
  • signing input — canonical JSON of actor, action, target, payload and signed_at.
  • verify_chain() — replays every row against the public verify key in one pass.

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, viewer, gate-only, audit-only.

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. Viewer reads cameras, plates, groups, rules and events. Gate-only fires the gate. Audit-only reads and exports the chain.

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="<your-secret-here>",  # rotate on suspected compromise, not on a calendar schedule
        ),
    },
)

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. Nine views: dashboard, live, cameras, events, groups, rules, audit, network, MFA. 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 — Coming capabilities

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 passing.

Maturity note: These five modules are reference implementation / pilot-ready today. Production connectors (live RTSP ingest, WireGuard mesh, cloud VLM, S3 OTA channels) land at M1–M3 milestones as noted per module. Available in early access — request early access to get started.

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.

Cloud egress notice: This is the one exception to LAN-only operation. When VLM fallback is enabled, plate-crop images leave your network and are sent to the configured cloud model. The feature is off by default and requires explicit operator opt-in.

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 — 202 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. 202 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, supported deployment SKU, multilingual dashboard, EFF-reviewed privacy posture.

08 — Stated commercial policy

Three lines we won't cross.

Our stated commercial policy — and will be written into the EULA at GA. 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 our stated commercial policy and will be in the EULA at GA, not just the marketing.

Get started

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

EFF privacy-posture review planned for M3 GA. SQLite + WAL. Ed25519 audit chain. LAN-only operation. Argon2id + TOTP. NaCl SecretBox backups. Sleep well.