# HEART Part 4: Connections

**How to reach a HEART, and how messages travel safely**

**Author:** Milind K. Patil, Syncaissa Systems Inc.
**Status:** Working draft 1, November 2026. Part of the [HEART Standards Family](HEART-Standards-Family.md). Includes the corrections of [Security Advisory HEART-2026-001](HEART-Security-Advisory-2026-001.md).
**Version:** 1.0 (draft)
**Read first:** [Part 1: Basics](HEART-Part-1-Basics-1.0.md) and [Part 3: Information Model](HEART-Part-3-Information-Model-1.0.md).
**Track:** Basic: "In one minute", Sections 1, 2, 3.1 to 3.3, 3.5, 4.1 to 4.3, 4.6, 5, 6.6, 7.1 and 13, and 12 for microcontrollers. Intermediate: the rest (3.4, 4.4, 4.5, 6.1 to 6.5, 7.2 to 7.4, 8 to 11).

---

## In one minute

Part 3 says **what** a HEART says. This part says **how it travels**.

A HEART can be reached in five ways:

| Connection | Use it for |
|---|---|
| **HTTP** | Almost everything: apps, services, devices running Linux |
| **Gateway** | Many systems sharing one address, and systems that cannot be reached directly |
| **Unix socket** | Programs on the same machine |
| **MQTT** | Connected devices that already use a message broker |
| **Serial line** | Microcontrollers, through a bridge |

The rules that matter most:

1. **Port 43278** is HEART's standard port, with **7685** as the fallback.
2. **Discovery needs no login.** `GET /.well-known/heart-card.json` always works, and on a local network systems announce themselves as `_heart._tcp`.
3. **Everything off the machine is encrypted** with TLS, and the device proves who it is.
4. **A login token works for one system only**, and only for whoever holds its key.
5. **The same messages and rules apply on every connection.** Changing the connection never changes what is allowed.

---

## 1. What this part covers

Think of Part 3 as the **letter**, and this part as the **postal service**. The letter says the same thing whether it goes by post, by courier or by hand. But each route has its own rules: addresses, stamps, signatures on delivery.

| This part covers | Other parts cover |
|---|---|
| Ports, addresses and how to find a system | The messages themselves (Part 3) |
| HTTP: paths, headers, paging, errors, live streams | Who must approve what (Part 6) |
| Logging in: tokens and scopes | What data may leave (Part 5) |
| Encryption and device identity | The meaning of Autonomy Levels (Part 2) |
| System-to-system messages and webhooks | |
| The HEART Gateway, Unix sockets, MQTT and serial lines | |

**The one rule above all others:** every connection carries the same operations and messages, under the same checks. A system MUST NOT be more open on one connection than on another.

---

## 2. The connections at a glance

A system MUST offer at least one of `http-server`, `http-push`, `unix-socket` or `mqtt`, and list them in its Card's `bindings`.

| Binding | What it means | Typical system |
|---|---|---|
| `http-server` | The system answers HTTP requests itself | A service, an app back end, an embedded Linux board |
| `http-push` | The system cannot be reached, so it **sends** its messages to a HEART Gateway, which serves them | A browser app, a phone app, a device behind a home router |
| `unix-socket` | HTTP over a local socket file, with no network port | A program behind a Gateway on the same machine |
| `mqtt` | Messages on an MQTT 5 broker | A fleet of connected sensors |
| `in-process` | Inside one program, such as a page and its Web Worker | A browser app. Listed for discovery only: it does not meet the requirement above. |

A microcontroller with only a serial port takes part through a **bridge**, which serves `http-server` for it (Section 12).

---

## 3. Finding a system

### 3.1 The first request

Every consumer starts the same way:

```
GET /.well-known/heart-card.json
```

This MUST work **without logging in**, and MUST return the public view of the Card. The Card then tells the consumer everything else: the API's address (`endpoints.api`), how to log in (`auth`), and other standards the system also speaks, such as MCP, A2A or OpenTelemetry.

The Card MUST come with a signature, in the `Heart-Card-Signature` header (Section 7.3), at Connected level and above.

### 3.2 Ports

| Name | Port | Role |
|---|---|---|
| **HEART** | `43278` | The standard port. It spells HEART on a phone keypad. |
| **Fallback** | `7685` | Used only when 43278 cannot be bound or reached. |

Both ports are the same service, with the same rules. The fallback is never a weaker way in.

Systems that face the internet MAY use port `443` instead, behind normal HTTPS infrastructure.

**Rules for a system (listener):**

1. Listen on 43278. If that port cannot be bound, use 7685.
2. When running on 7685, record `system.port.fallback` in the Rhythm, and show the real port in the Card.
3. On Linux, reserve 43278 so outgoing connections cannot take it: `sysctl net.ipv4.ip_local_reserved_ports=43278`.

**Rules for a client:**

1. If the address names a port, use only that port.
2. If not, try 43278, then 7685. A connect timeout of 3 seconds is recommended.
3. Fall back **only if the connection itself fails**: refused, unreachable or timed out.
4. **Never** fall back after a certificate failure, a login failure or any HTTP reply. Those are answers, not outages, and trying elsewhere could hand your token to an impostor.
5. Check the same server identity on both ports.

### 3.3 The `heart://` address

`heart://` is a short way to write an address. It is not a new protocol: it always means HTTPS.

| Address | Means |
|---|---|
| `heart://plant-server` | `https://plant-server:43278/.well-known/heart-card.json` |
| `heart://plant-server/systems/pump-station` | The Card of `pump-station`, at the Gateway on `plant-server` |
| `heart://plant-server/systems/pump-station/pulse` | `https://plant-server:43278/heart/v1/systems/pump-station/pulse` |

The rule: add `/heart/v1` before the path, and use port 43278 if none is given. An address ending at a system name means that system's Card.

### 3.4 Finding systems on a local network

On a home or factory network, nobody knows the devices' addresses in advance. HEART systems announce themselves with DNS-SD over multicast DNS (*mDNS*), the method printers and smart speakers already use.

**The service type is `_heart._tcp`**, on the HEART port. A TXT record gives a few public facts, so a tool can list devices without contacting each one:

| TXT key | Meaning | Example |
|---|---|---|
| `txtvers` | Version of this record. Always `1`. | `1` |
| `id` | System ID | `com.example.cool-air.ac-controller` |
| `path` | Where the public Card is | `/.well-known/heart-card.json` |
| `level` | Feature Set | `full` |
| `tls` | `1` if the HEART uses TLS | `1` |
| `tpl` | Template ID, if built from one (optional) | `heart/embeddedstandard/c-posix/v1.2611` |
| `kt` | Thumbprint of the identity key (optional) | `NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs` |
| `gw` | `1` if this is a HEART Gateway (optional) | `1` |

Rules:

1. A system that serves HEART on a local network SHOULD announce `_heart._tcp`. A Gateway announces itself, not each system it hosts.
2. The TXT record MUST contain **only** what the public Card already shows. It MUST NOT identify a person, and the instance name MUST NOT either.
3. A system MUST NOT announce a HEART that is not reachable on that network. For example, one that listens only on loopback must stay silent.
4. **Discovery is a hint, never proof.** Anyone on a network can announce anything. A client MUST still check the system's certificate and signed Card (Section 7) before trusting what it found. If `kt` is given, it SHOULD match the Card's identity, and a mismatch is a warning sign.
5. The owner MUST be able to turn announcements off.

Tools: `heart-templates/tools/heart_mdns.py` finds HEART systems or announces one (standard library only), and `heart-templates/tools/avahi/heart.service` announces a Linux device through Avahi.

### 3.5 Watching a device from anywhere: HEART Hub

A device that cannot be reached from outside can still be watched: it **reports** its public Pulse and alerts, outbound only, to a monitor site that its owner registered it with. Each device then has a private page, such as `https://heartcortex.ai/device/3f9c0a71b2d4e865`, that its owner may share. The rules, for devices and for sites, are in the [HEART Hub profile](HEART-Monitor-1.0.md). Anyone may run a monitor site with the open code in `heart-monitor/`.

A small sensor need not build envelopes: it may send a plain JSON **light report** (status, alerts by fault code, and figures), and may say how long it will sleep, so its silence is not taken for a Flatline. It may send it over HTTPS, or over MQTT through a small bridge that relays it to the site. See HEART Hub, Sections 5.4 to 5.6 and 7.

---

## 4. Talking HTTP

### 4.1 The basics

| Topic | Rule |
|---|---|
| **Base path** | `/heart/v1` |
| **Messages about the system** | Sent as CloudEvents envelopes (Part 3), `Content-Type: application/cloudevents+json` |
| **Lists** | `{"items": [...], "nextCursor": ...}`, as `application/json` |
| **Requests to the system** (invoke, ask, decide) | Plain JSON bodies |
| **Version** | Every response MUST carry `Heart-Version: 1.0` (or the minor version in use). A request for an unsupported major version gets `400 unsupported-version`. |
| **Not offered** | An operation above the system's Feature Set, or in a module it does not claim, MUST answer `501 not-implemented` |

### 4.2 The main operations

The full list of 124 operations is in the OpenAPI file, [`openapi/heart-api-1.0.yaml`](openapi/heart-api-1.0.yaml). These are the ones most consumers use:

| Request | What it does | Feature Set |
|---|---|---|
| `GET /.well-known/heart-card.json` | The public Card | Basic |
| `GET /heart/v1/pulse` | The Pulse | Basic |
| `GET /heart/v1/arrhythmias` | List alerts | Basic |
| `GET /heart/v1/card` | The Card, as much as you may see | Connected |
| `GET /heart/v1/rhythm` | History, or a live stream | Connected |
| `POST /heart/v1/ask` | Ask a question in plain language | Connected |
| `POST /heart/v1/inbox` | Send a message from another system | Connected |
| `POST /heart/v1/subscriptions` | Ask to be sent messages (webhooks) | Connected |
| `GET /heart/v1/receipts` | What data left, and to whom | Connected |
| `GET /heart/v1/capabilities` | What it can be asked to do | Connected |
| `POST /heart/v1/capabilities/{name}/invoke` | Ask it to do something | Full |
| `POST /heart/v1/actions/{id}/decision` | Approve, deny or cancel | Full |
| `POST /heart/v1/tending/stop` | Stop all automatic action | Full |

**The Pulse always answers.** `GET /heart/v1/pulse` MUST return `200` whenever the HEART can respond at all, *even when the system is failing*. The bad news is in the body, not in the status code.

### 4.3 Headers

| Header | Direction | Rule |
|---|---|---|
| `Heart-Version` | Response | Required |
| `Idempotency-Key` | Request | Required on every `POST` that is not `read`, except `acknowledge`, `resolve`, `silence` and an Action's `decision`; without it, `400 invalid-request`. Optional on `PUT`, `PATCH` and `DELETE`, which set or remove a whole value; with a key, a retry of one that waits for approval returns the same Action. Keys belong to one caller and one operation. Sending the same key again returns the same result, and never runs it twice. The system MUST remember keys for at least 24 hours. The same key with a different body gets `422 idempotency-conflict`. |
| `Authorization` | Request | The access token (Section 6) |
| `DPoP` | Request | Proof that the caller holds the token's key (Section 6.3) |
| `Heart-Card-Signature` | Response | The Card's signature (Section 7.3) |
| `Heart-Key-Rotation` | Response | For 7 days after a key change: the statement, signed by the old key, that names the new one (Section 7.4) |
| `Heart-Receipt-Salt` | Both | The salt of the Valve receipt, when non-public data goes to a receipted recipient (Part 5) |
| `Heart-Principal`, `Heart-Clearance`, `Heart-Scopes`, `Heart-Caller` | Request | Only from a Gateway to a hosted system, signed (Section 9.4) |
| `traceparent` | Both | W3C Trace Context. Systems SHOULD accept and pass it on. |
| `ETag`, `Cache-Control` | Response | The Card SHOULD carry them |
| `Retry-After` | Response | With `429 rate-limited` |

### 4.4 Paging

Long lists come a page at a time. The reply has a `nextCursor`. Send it back as `?cursor=...` for the next page. `nextCursor: null` means the end. A page holds at most 500 items, and 100 by default.

If history was deleted to save space, the page MUST say `gap: true`, so nobody mistakes missing history for nothing happening.

### 4.5 Live streams

`GET /heart/v1/rhythm` with `Accept: text/event-stream` keeps the connection open and sends each new event as it happens (Server-Sent Events). Each event has an ID. After a disconnect, reconnect with `Last-Event-ID` and the stream continues where it stopped, with nothing lost.

### 4.6 Limits

Every operation MUST be rate-limited, including discovery and the Pulse. Too many requests get `429 rate-limited` with `Retry-After`.

---

## 5. Errors

Every error uses the same format (RFC 9457), with a stable `code` that programs can rely on:

```json
{
  "type": "https://heartcortex.ai/problems/insufficient-clearance",
  "title": "Insufficient clearance",
  "status": 403,
  "detail": "Reading rhythm requires internal clearance.",
  "code": "insufficient-clearance"
}
```

| Code | HTTP | Meaning |
|---|---|---|
| `invalid-request` | 400 | The body or parameters are wrong |
| `unsupported-version` | 400 | This major version is not spoken here |
| `unauthenticated` | 401 | No login, or the token is invalid, expired or for another system |
| `token-not-bound` | 401 | Over a network, the token must be bound to the caller's key |
| `insufficient-clearance` | 403 | Not allowed to see this, or missing a scope |
| `capability-not-allowed` | 403 | This kind of caller may not do this |
| `caller-not-allowed` | 403 | A Proposal names a target closed to AI. Only Proposals get this code |
| `presence-required` | 403 | A person must prove they are present for this exact action (Part 6) |
| `presence-invalid` | 403 | The proof of presence did not check out |
| `valve-blocked` | 403 | The Valve refused to release the data (Part 5) |
| `signature-required` | 403 | A Connected or Full system must sign the Card it joins with |
| `signature-invalid` | 403 | The Card's signature does not check out |
| `not-found` | 404 | No such thing, or you may not know it exists |
| `conflict` | 409 | Wrong state, such as deciding a finished action |
| `identity-changed` | 409 | The system joined before with a different identity key |
| `autonomy-stopped` | 409 | Refused because automatic action is stopped |
| `guard-failed` | 409 | A guard or interlock refused the command |
| `idempotency-conflict` | 422 | A retry key was reused with a different body |
| `out-of-range` | 422 | A value is outside what is allowed |
| `cannot-undo` | 422 | A trial change was refused because it could not be undone |
| `budget-exceeded` | 429 | The AI spending limit was reached |
| `rate-limited` | 429 | Too many requests |
| `internal-error` | 500 | Something unexpected went wrong |
| `not-implemented` | 501 | Not offered by this system |
| `native-failed` | 502 | The device returned an error |
| `system-unreachable` | 503 | A Gateway could not reach the hosted system |
| `model-unavailable` | 503 | No AI model could be reached |
| `native-timeout` | 504 | The device did not answer in time |

---

## 6. Logging in

### 6.1 Tokens

A caller logs in with an **access token** from the owner's login service, using OAuth 2.0 (client credentials), or with a client certificate (mutual TLS). A token is a signed note that says who the caller is and what they may do.

| What the token says | Claim | Rule |
|---|---|---|
| **Who** | `sub` | The caller's identity |
| **Which system it is for** | `aud` | MUST name this system's ID. A system MUST refuse a token for any other system. |
| **What they may see** | `heart_clearance` | `public`, `internal` or `private` |
| **What kind of caller** | `heart_caller` | `human`, `peer`, `ai` or `rule`. Set by the login service from how the caller was registered, never chosen by the caller. If missing, the caller counts as `peer`. |
| **What they may do** | `scope` | The scopes below |
| **Acting for someone else** | `act` | If present, the caller is an AI agent (Section 6.4) |
| **Whose key it is bound to** | `cnf.jkt` | The fingerprint of the holder's key (Section 6.3) |
| **Who issued it** | `iss` | MUST be the issuer named in the Card's `auth.issuer` |
| **When it expires** | `exp` | At most 15 minutes after issue. A system MUST refuse an expired token, allowing at most 60 seconds of clock difference. |

Tokens are JWTs as RFC 9068 describes. `iss`, `aud`, `exp`, `sub`, `scope`, `heart_clearance` and `heart_caller` MUST all be present, and `cnf` when the token is bound to a key; a token missing any gets `401 unauthenticated`. The system checks the signature with the issuer's published keys. A token cannot be called back once issued, which is why it lives so briefly. A Full system MAY also ask the issuer about each token (introspection, RFC 7662), for owners who need to cut access at once.

**Client certificates.** A certificate names its caller in a SAN URI. The owner keeps a table that gives each one its clearance, scopes and caller kind: the grants that name a certificate (Section 6.5). A certificate not in the table counts as `peer`, clearance `public`, with no scopes.

### 6.2 Scopes and clearance

There are two separate checks on every request:

- **Scope** says what a caller may **do**.
- **Clearance** says what a caller may **see**.

| Scope | Allows |
|---|---|
| `heart.read` | Reading anything above the public view, up to the caller's clearance. Without it, a caller gets the public view. |
| `heart.ask` | Asking questions |
| `heart.send` | Sending messages to the inbox, and joining a Gateway |
| `heart.subscribe` | Managing webhooks |
| `heart.tend` | Acknowledging, resolving, silencing and reporting alerts, and stop |
| `heart.propose` | Delivering a brain's Proposal. Nothing else, and never together with `heart.approve` |
| `heart.invoke` | Asking for actions |
| `heart.approve` | Approving actions. Never given to AI agents. |
| `heart.reason` | Calling a Reasoning Gateway |
| `heart.configure` | Changing properties, settings, schedules and custom commands |
| `heart.operate` | Diagnostics, power and software operations |
| `heart.admin` | Policies, autonomy, the allowlist, keys, peers and Playbooks |
| `heart.data` | Consent and personal data requests |
| `heart.fleet` | Fleet commands and hosted systems at a Gateway |
| `heart.native` | Raw device protocol access |

A system with no login at all may still conform at Basic, serving public views only.

### 6.3 A token is bound to its holder

A token that anyone can copy and use is like a house key without a lock on the copying machine. So:

1. **Over a network, every token MUST be bound** to a key the caller holds, with DPoP (RFC 9449) or a client certificate (RFC 8705). Only connections that never leave the machine (loopback and Unix sockets) are excused. An unbound token from the network gets `401 token-not-bound`.
2. With DPoP, each request carries a fresh `DPoP` proof: a short note signed with the caller's key, naming the method, the URL, the time and the token. The system MUST check that the key matches `cnf.jkt`, that the proof is under a minute old, and that it has not been used before.
3. A token bound to a key and sent without proof of that key MUST be refused.

So a stolen token is useless without the key, and a captured proof cannot be replayed.

### 6.4 AI agents are never people

People increasingly connect AI assistants to their accounts. An assistant then holds a token that says `human`. The rules:

1. A token issued to a client registered as an agent, or whose `act` claim (delegation, RFC 8693) names any actor that is not a HEART Gateway marked `heart_gateway: true` by the issuer, MUST be treated as `ai`, whatever `heart_caller` says. A token whose only actors are marked Gateways keeps the subject's `heart_caller` (API Standard 8.2).
2. Such a token MUST NOT carry `heart.approve`.
3. A person's own token can still be pasted into an agent, so approvals also need **proof of presence** (Part 6).

### 6.5 Roles: who may call a system

An EV charger is shared with its installer, a pump station with the operators on call, a transmitter with a clinic. The owner gives each of them a **grant** (`PUT /heart/v1/access/grants/{principal}`, module `access`), and each grant gives one **role**:

| Role | May do | May see |
|---|---|---|
| `owner` | Everything but deliver a brain's Proposal | `private` |
| `installer` | Read, tend, invoke, configure and operate | `internal` |
| `maintainer` | Read, tend, invoke and operate | `internal` |
| `operator` | Read, ask, subscribe, tend, invoke and approve | `internal` |
| `viewer` | Read and ask | `internal` |

1. While a system holds any grant, a caller can do only what both its token and its role allow, and see only what both allow. A caller with no grant counts as a stranger: `peer`, `public`, no scopes.
2. A grant may name a client certificate. The grants are then the certificate table of Section 6.1.
3. Giving access, or widening it, is `destructive`: the owner proves presence (Part 6, Section 4.1). Narrowing or removing access is `write`.
4. The first grant gives the `owner` role, and the last owner grant cannot be removed. Nobody can lock the owner out by accident.

### 6.6 Small devices: the owner key

A thermostat or a birdhouse has no login service. Below HEART Connected, a system MAY use one **owner key** instead of tokens (API Standard 8.2):

1. The device makes the key at first start, with at least 128 random bits, and shows it once, only at the device: its screen, its console, or a card in the box.
2. It keeps only a hash of the key, and compares a presented key by that hash, in constant time.
3. The owner sends it as `Authorization: Bearer <owner key>`, over TLS or within one machine. Whoever holds it is the owner, with clearance `private`.
4. The owner can replace the key at the device. The old key stops working at once. Releasing, transferring or resetting the device replaces it too.
5. The key never leaves the device: not to a HEART Hub, not to a Command Center, not to the maker.
6. From HEART Connected up, the owner key is not accepted. Tokens bound to their holder are.

---

## 7. Encryption and identity

### 7.1 Encryption

All traffic MUST use TLS 1.2 or later, except inside one machine (loopback, Unix sockets, in-process). A system MUST refuse to serve plain HTTP on any network address.

### 7.2 Every device has an identity

Encryption only helps if the client can tell the real device from an impostor. A device on a home or factory network has no public name for an ordinary certificate. So:

1. **Identity key.** Every system at Connected level or above MUST have its own key pair. The private key never leaves the system, and SHOULD live in secure hardware where there is any.
2. **Maker certificate.** A device SHOULD carry a certificate for that key from its maker (IEEE 802.1AR style).
3. **Claiming.** The one-time code for taking ownership MUST be unique per device, come in the box (a label or QR code), and never be derived from public data such as a serial number. Claiming SHOULD use SPAKE2+, as Matter does, so the code cannot be captured on the network. The owner records the device's key when claiming.
4. **Checking the server.** A client MUST accept a certificate only if it comes from the owner's or maker's authority, or its key matches the one recorded at claim. Accepting an unknown key on first contact is not allowed. Products as shipped MUST NOT have any switch, test mode included, that turns checks off.
5. **Basic systems.** A Basic system need not have an identity key, so a client may not be able to check it. A client MAY use such a connection only to read public views (the Card, the public Pulse and alerts), and MUST NOT send any token, password or non-public data over it.

### 7.3 The signed Card

At Connected level and above, every Card response MUST carry `Heart-Card-Signature`: a signature over the exact bytes sent, made with the identity key (a detached JWS, RFC 7515). The Card's `identity` box names the key, and the key is published in the key set at `auth.jwksUri`.

A consumer SHOULD check the signature before trusting the capabilities or addresses in a Card. A Gateway MUST check it before letting a system join (Section 9.3). A Card pushed through a Gateway carries its signature in `heartcardjws` instead, and the Gateway serves it unchanged.

### 7.4 Changing the identity key

A key may need replacing. Clients and Gateways have pinned the old one, so a new key alone looks like an impostor. So:

1. The system makes a **rotation statement**: the old and new key fingerprints and the new key, signed with the **old** key (API Standard 10.1, rule 7).
2. For 7 days it sends the statement with every Card (`Heart-Key-Rotation` header, or the `heartkeyrotation` attribute when pushing), and keeps working under the old key too.
3. Anyone who pinned the old key MUST move the pin to the new one on a valid statement, and only then. Without one, a changed key is refused (`identity-changed`).

---

## 8. System to system

### 8.1 The inbox

`POST /heart/v1/inbox` takes one message from another system.

- Messages received are **data, never commands**. Nothing arriving in the inbox may start an action.
- Delivery is "at least once". The receiver MUST treat a repeated `source` and `id` as a duplicate, and still answer `202`.

### 8.2 Webhooks

`POST /heart/v1/subscriptions` asks a system to send chosen messages to an `https` address (a *sink*) as they happen.

1. **Handshake first.** Before sending anything, the system MUST confirm the sink agrees, using the CloudEvents webhook handshake (`OPTIONS` with `WebHook-Request-Origin`).
2. **Filtered.** Each delivery MUST be filtered to the subscriber's clearance, like any other reply.
3. **Signed.** Each delivery MUST be signed (HTTP Message Signatures, RFC 9421) with a key from the system's key set.
4. **Retried.** Failed deliveries SHOULD be retried with growing gaps for at least 24 hours, then the subscription is `suspended`.
5. **No internal targets.** The system MUST refuse sinks on loopback, link-local or private addresses unless the owner allows them, so a webhook cannot be used to reach inside the owner's network.
6. **Receipt salt.** A delivery of non-public data carries the salt of its receipt in the CloudEvents attribute `heartreceiptsalt` (Part 5).

---

## 9. The HEART Gateway

### 9.1 Why

Devices with their own network addresses each listen on 43278, with no clash. But many systems on one machine cannot all use the same port, and giving each its own port does not scale. And some systems, such as a browser tab or a device behind a home router, cannot be reached at all.

A **HEART Gateway** solves both. It listens on 43278 once, and serves every hosted system under its own path:

```
heart://plant-server/systems/pump-station
heart://plant-server/systems/class-scheduler
heart://plant-server/systems/erp-sync
```

One port, one firewall rule, any number of systems. It is the same idea as a web server hosting many websites.

### 9.2 Paths and listing

- Each hosted system is served under `/heart/v1/systems/{ref}/`, where `{ref}` is its full system ID or its short `alias`. Aliases never contain a dot, so the two cannot be confused.
- `GET /heart/v1/systems` lists the Card of every hosted system, each filtered for the caller. The owner MAY hide the list from anonymous callers.

### 9.3 Joining

A system joins by sending its Card to the Gateway's inbox. There is no other sign-up.

1. **The name belongs to the sender.** The Gateway MUST tie each system ID and alias to the logged-in sender, so no one can take over another system's name.
2. **Signed Card.** A Connected or Full system MUST join with a Card signed by its identity key. When pushing, it carries the signature as an attached JWS, including its public key, in the CloudEvents attribute `heartcardjws`. The Gateway MUST check the signature, check that the key matches the Card's `identity`, and refuse anything else (`signature-required`, `signature-invalid`).
3. **Pinned key.** The Gateway remembers the key a system first joined with. A later join with a different key MUST be refused with `identity-changed`, unless it comes with a valid rotation statement (Section 7.4), or the owner has removed the system.
4. **The Gateway's own key.** When joining, the system reads the Gateway's Card over a checked connection and remembers the Gateway's key. Caller headers (Section 9.4) are checked against that key.
5. **Routed or pushed.** A Card that gives `endpoints.api` (https) or `endpoints.socket` makes the system **routed**: the Gateway passes each request to it live. A Card with neither makes it **pushed**: the Gateway stores and serves what the system sends. The Gateway MUST apply the same address restrictions as for webhooks.

### 9.4 Passing on the caller

The Gateway checks the caller's login. The hosted system still needs to know who the caller is, so its own Valve can filter the reply.

1. The Gateway passes the verified caller in four headers: `Heart-Principal` (the caller's `sub`), `Heart-Clearance` (`public`, `internal` or `private`), `Heart-Scopes` (the scopes, separated by spaces) and `Heart-Caller` (`human`, `peer`, `ai` or `rule`). The exact formats are in API Standard 9.3.
2. **Over a network**, the Gateway MUST sign these headers (RFC 9421, label `heart`), covering the method, the full address, the four headers and the time (`created`), with its identity key. The system MUST check the signature against the Gateway key it remembered at join, and refuse one older than 60 seconds.
3. **The Gateway MUST NOT pass on the caller's own token.** A hosted system chose its own address when it joined, so it could capture the token and use it elsewhere. If a system needs a token, the Gateway gets a new one made for that system only (token exchange, RFC 8693), with no more rights than the caller had.
4. The Gateway MUST remove these four headers from every incoming request, and a system MUST trust them only when signed by its Gateway, or from its Gateway's verified process on a Unix socket (Section 10).
5. The Gateway MUST NOT serve one caller's non-public reply to a different caller.

**The Valve stays in each system.** The Gateway may handle encryption, logins and rate limits in one place, but filtering happens inside each system, before data leaves it. A compromised Gateway therefore sees only what each caller was already allowed to see.

### 9.5 Pushed systems

1. The system's own Valve filters before pushing, so only what was pushed is ever available. It pushes one copy of each message for each clearance level it shares.
2. The Gateway MUST NOT change or filter pushed messages itself. It gives each caller the copy for the highest level that caller may see, byte for byte, or nothing. It MUST serve the last Pulse unchanged, so consumers can apply the Flatline rule.
3. `ask` needs the live system, so the Gateway answers `501` for a pushed system.
4. A pushed system SHOULD keep a bounded buffer of messages while the Gateway is unreachable, and resend them with their original IDs.

**Commands through the channel the device already holds.** A device behind a home router or a mobile network cannot be reached from outside, but it can keep asking its Gateway for work. That is its **command channel** (API Standard 9.3):

1. The device sends its messages to the Gateway's inbox with `Prefer: wait=25`. The Gateway holds the answer until a command is waiting, or 25 seconds pass, and then answers with the commands. The device asks again at once.
2. A caller sends an ordinary HEART request to the device's Gateway path. The Gateway turns it into a **command**: the request, the caller it checked, an ID, and a lifetime of at most 60 seconds, all signed with the Gateway's identity key.
3. The device checks the signature against the Gateway key it remembered at join, checks that the command is for it, still fresh, and new to it, and drops anything else without answering. It then runs the request through its own API and gate, exactly as if it had come in from the Gateway, and sends the answer back as a `heart.reply`. The Gateway gives that answer to the caller.
4. Nothing about safety moves to the Gateway. The device still checks scope and clearance, filters with its own Valve, needs proof of presence where the gate does, and obeys Stop.

A pushed system with a command channel can be commanded, so it may claim HEART Full. One without a channel cannot be commanded through the Gateway, and stays at Connected.

### 9.6 The Gateway is a system too

A Gateway MUST have its own Card and Pulse, and follows this standard like any other system. It is also a shared point of failure, so:

1. Hosted systems MUST keep looking after themselves when the Gateway is down.
2. Production deployments SHOULD run more than one Gateway.
3. A Gateway SHOULD raise a `system.flatline` alert for a hosted system that goes quiet for three Pulse intervals.

---

## 10. Unix sockets

Programs on the same machine as a Gateway can talk to it through socket files instead of network ports. The protocol is the same HTTP, without TLS.

| Socket | Who listens |
|---|---|
| `/run/heart/gateway.sock` | The Gateway, for systems joining or pushing |
| `/run/heart/systems/{systemId}.sock` | A routed system, for requests from the Gateway |

1. Sockets MUST NOT be open to every user on the machine.
2. **File permissions are not enough.** Both sides MUST check the other process's identity through the operating system (`SO_PEERCRED` or its equivalent). A system's socket MUST accept the Gateway's caller headers only from the Gateway's own user. Where this check is not available, the headers must be signed, as over a network.
3. Socket paths are `internal`, so they never appear in a public Card.

---

## 11. MQTT

For devices that already use an MQTT 5 broker. MQTT uses the broker's own port, not 43278 or 7685. `{instance}` is `-` for a system with no instance, and `{level}` is `public`, `internal` or `private`.

| Topic | Carries | Kept on the broker? |
|---|---|---|
| `heart/v1/{systemId}/{instance}/card/{level}` | The Card | Yes |
| `heart/v1/{systemId}/{instance}/pulse/{level}` | The Pulse | Yes |
| `heart/v1/{systemId}/{instance}/rhythm/{level}` | Events | No |
| `heart/v1/{systemId}/{instance}/arrhythmia/{level}` | Alerts | No |
| `heart/v1/{systemId}/{instance}/cmd/{operationId}` | Commands to the device | **Never** |
| `heart/v1/{systemId}/{instance}/reply/{clientId}` | Replies to one client | No |

- Messages use the CloudEvents MQTT binding, structured JSON. Commands have the CloudEvents `type` `heart.command.{operationId}`, with `data` = `{"path", "query", "body"}` of the HTTP request. A command sets MQTT 5's Response Topic and Correlation Data; the reply, `type` `heart.reply`, has `data` = `{"status", "headers", "body"}` of the HTTP response, so every status and problem code means the same as over HTTP.
- **One topic per level.** A device publishes one copy of each message for each level it shares, filtered to that level. The broker's access rules MUST let a client read a `{level}` topic only with that clearance or higher. A device that publishes internal data on a public topic does not conform.
- A device SHOULD set an MQTT "last will" that publishes a public Pulse with status `unknown`, so its silence is noticed.
- A device that only reports to HEART Hub may instead publish its light report on `heart/v1/hub/{deviceId}/report`, for a bridge to relay over HTTPS. Those topics and their rules are in HEART Hub, Section 5.6.

### 11.1 Commands must be signed

The broker's rules say who may publish on a topic. They say nothing about **who sent this particular command**, what they may do, or whether it is a recording being played again. So every command MUST:

1. **Carry a token** for this device (`aud` = its system ID), in the MQTT user property `heart-token`.
2. **Be signed** with the key that token is bound to, as a compact JWS (`ES256` or `EdDSA`) in the MQTT user property `heart-sig`. Its payload is `{"sys", "op", "sha256", "id", "exp"}`: the system ID, the operation, base64url SHA-256 of the exact `data` bytes, the message ID, and an expiry at most 60 seconds ahead (seconds since 1970). The exact rules are in API Standard 9.5.
3. **Be checked** by the device exactly as over HTTP: scope, clearance and the approval gate.
4. **Be refused** if it has expired (allowing 60 seconds of clock difference), if any claim does not match the message, or if its ID has been seen before.

A device too small to check signatures MUST NOT take commands over MQTT. It is commanded through a Gateway instead. Replies SHOULD be signed with the device's identity key, in the same form.

---

## 12. Small devices: HEART Line v1

A microcontroller is too small to serve web requests or check signatures. It still has a HEART: it notices its own problems. It reports them as short lines on a serial port, and a **bridge** on a nearby computer serves HEART for it. This is the delegation pattern of the HEART Core Standard, Section 8.

```
 microcontroller ──serial line──► bridge ──HTTP──► anyone asking
  notices problems itself          adds time, IDs,   └──► HEART Gateway (optional)
                                   serves HEART Basic
```

### 12.1 The lines

One JSON object per line: UTF-8, at most 1024 bytes including the line end, ending with a newline (`\n`; a `\r` before it is ignored). The serial settings are the device's own; 115200 baud, 8 data bits, no parity, 1 stop bit is RECOMMENDED. Every line has `t`, its type.

| Line | When | Example |
|---|---|---|
| `card` | Once, at start | `{"t":"card","id":"org.example.my-device","name":"My Device","version":"0.1.0","instance":"unit-1","template":"heart/embeddedstandard/c-baremetal/v1.2611","interval":30}` |
| `pulse` | Every interval | `{"t":"pulse","seq":12,"status":"healthy"}` |
| `alert` | When a problem starts | `{"t":"alert","code":"system.main-function-lost","severity":"critical"}` |
| `resolved` | When it ends | `{"t":"resolved","code":"system.main-function-lost"}` |

| Field | Meaning |
|---|---|
| `interval` | Seconds between `pulse` lines, 1 to 86400 |
| `seq` | Counts up by one per `pulse` line, starting at 0 after each `card` line |
| `status` | `healthy`, `degraded` or `failing` |
| `code` | A fault code (Part 3). A device has at most one open problem per code. |
| `severity` | `info`, `warning`, `error` or `critical`. Default `error`. |
| `text` | Optional plain-language explanation, at most 200 characters |

A `card` line starts a new instance, as after a reboot: open problems from before it are resolved by the bridge, and `seq` starts again.

### 12.2 Rules for the device

1. The device MUST notice problems and send `alert` and `resolved` itself, so it keeps working with no network.
2. It MUST send a `card` line at start, and a `pulse` line at least once per `interval`.
3. It MUST NOT accept commands over the serial line in version 1.0. HEART Line v1 carries reports only.

### 12.3 Rules for the bridge

1. The bridge MUST serve nothing for a device until it has received a `card` line.
2. It MUST turn each line into the matching Part 3 message, adding the time, a unique ID and the source. An `alert` becomes an Arrhythmia with that `code` and `severity`, a new UUID, `detectedAt` set to when the line arrived, and `text` (or the code's meaning from Part 3) as `explanation`. Each `pulse` becomes a Pulse with the bridge's own `sequence`, which keeps rising across device restarts.
3. It MUST raise an alert only once per open problem, and resolve it on `resolved`.
4. If no line arrives for three intervals, the device's status MUST become `unknown` (the Flatline rule).
5. It MAY push the messages on to a HEART Gateway, logged in as the device's owner.
6. The Card it serves keeps the device's `template` value, so everyone can see what the device runs.
7. A line that is not valid JSON, is too long, or has an unknown `t` MUST be ignored and counted, never guessed at. Unknown fields are ignored.

Tested templates for both sides are listed in `heart-templates/README.md`.

---

## 13. Choosing a connection

| Your system | Use |
|---|---|
| A service or app back end with a network address | `http-server` on 43278, with TLS |
| Many services on one machine | A Gateway on 43278, with each service on a Unix socket |
| A browser app, phone app or device behind a home router | `http-push` to a Gateway |
| A fleet of devices already on MQTT | `mqtt`, with signed commands |
| A microcontroller with a serial port | HEART Line v1 and a bridge |
| A small sensor that only reports to HEART Hub | A light report over HTTPS, or over MQTT through a bridge (HEART Hub, Sections 5.4 and 5.6) |
| A service on the internet behind existing HTTPS | Port 443, with the same paths |

---

## 14. Changes needed in other files

| Change | Where |
|---|---|
| When the parts replace the API Standard, Sections 5, 8, 9 and 10.1 move here | `HEART-API-Standard-1.0.md` |
| Add `serial-line` as a recognised delegation link, without making it a Card binding (the bridge serves `http-server`) | `schemas/1.0/card.schema.json` description |
| Publish a schema for HEART Line v1 records | New `schemas/1.0/heart-line.schema.json` |
| Add the `heartcardjws`, `heartreceiptsalt` and `heartkeyrotation` CloudEvents attributes to the envelope schema (done in the API Standard, Section 3.1) | `schemas/1.0/envelope.schema.json` |

---

## 15. Open items

| Item | Why it matters |
|---|---|
| **Port registration** | Ports 43278 and 7685 are not yet registered with IANA. IANA usually gives one port per service, so 7685 may stay an unregistered convention. |
| **The `heart` URI scheme and well-known path** | Neither is registered yet. Both need a published specification first. |
| **The `heart` service name** | DNS-SD needs the service name `heart` registered in IANA's service name registry (RFC 6335), with the port. |
| **Compact binary (CBOR and CoAP)** | JSON may be too heavy for the smallest devices. A CBOR profile, with COSE signatures, is planned. |
| **Commands over HEART Line** | Version 1 lines carry reports only. Commands would need signing on devices that can check signatures. |
| **Levels per connection** | A system might offer Full on one connection and only Connected through a Gateway. A Card should be able to say so. |
| **Bluetooth and Matter** | Possible future connections, through the same rule: same messages, same checks. |
