Service Announcements
Services describe themselves to their owner, so that an inventory of what someone runs is a side effect of running it rather than a list kept by hand.
Each service publishes an encrypted, self-expiring record of what it is, where it can be reached and how it is doing. A reader holding the owner's key collects those records. There is no server to run and no prober: a reader need not reach a service to know whether it is alive.
Kinds
| kind | name | type | author | purpose |
|---|---|---|---|---|
30200 | Service Announcement | addressable | service | What a service is, where it is, how it is doing. |
10200 | Service Roster | replaceable | owner | Which service keys the owner accepts. |
Both are provisional and unassigned at the time of writing; 200 is a nod to HTTP's 200 OK.
Motivation
Self-hosted inventories are a hand-maintained list plus a prober that must reach every host. The list drifts from reality as things change, and the prober cannot see behind NAT without being inside the network, which is exactly when one wants to look from elsewhere. Inverting it fixes both: services announce, nothing polls, and because announcements are ordinary events the inventory can be read from anywhere, while using a service still requires whatever network it sits behind.
Roles
- owner: the person. One pubkey, SHOULD be dedicated to this purpose rather than a social identity.
- service: a process that announces itself, holding its own keypair generated on first run. A host may run many.
- reader: anything that displays the inventory. Needs the owner's key to decrypt, and SHOULD obtain decryption through a remote signer ([NIP-46](46.md)) rather than holding the secret.
Service Announcement
{
"kind": 30200,
"pubkey": "<service pubkey>",
"created_at": 1785400000,
"tags": [
["d", "7f3c1a9e-4b02-4c31-9a77-2e5d8c10b4aa"],
["p", "<owner pubkey>"],
["expiration", "1785400900"]
],
"content": "<NIP-44 ciphertext of the payload below>"
}| tag | presence | meaning |
|---|---|---|
d | MUST | Stable per service instance, and opaque (see Privacy). |
p | MUST | The owner. Readers subscribe by this. |
expiration | SHOULD | [NIP-40](40.md). See Liveness. |
No other tags are defined and implementations SHOULD NOT add descriptive ones: tags are public, and anything descriptive belongs in the payload.
content is the payload below, encrypted with [NIP-44](44.md) between the service key and the owner key.
{
"v": 1,
"name": "hanami",
"type": "blossom",
"version": "0.14.0",
"host": "box1",
"node": "<pubkey identifying the host, if not this key>",
"started_at": 1784800000,
"interval": 300,
"urls": [
{ "role": "admin", "url": "https://box1.example/admin" },
{ "role": "api", "url": "https://box1.example/upload" }
],
"metrics": { "storage_used": 44023414784, "storage_total": 64424509440 },
"note": "3 peers, last backup 2h ago"
}| field | presence | meaning |
|---|---|---|
v | MUST | Payload version. 1 for this document. |
name | MUST | Human name, usually the software. |
interval | MUST | Seconds between republications. Defines staleness (see Liveness). |
type | SHOULD | Broad category (relay, blossom, signer, …), for grouping. |
version | SHOULD | Version of the running software. |
host | SHOULD | Human label for the machine. A display hint (see Security). |
node | SHOULD | Pubkey identifying the host, where services and hosts differ. |
started_at | MAY | Unix seconds the service started, so uptime can be shown. |
urls | MAY | Addresses, each with a role. Reachability depends on the reader. |
metrics | MAY | A small map of current numeric gauges. Never a series (see Scope). |
note | MAY | Short free text for whatever does not fit. |
Readers MUST ignore unrecognised fields, and SHOULD render a payload with a higher v on a best-effort basis rather than discarding it.
Liveness
A service MUST republish every interval seconds and SHOULD set expiration to about three times interval. On clean shutdown it MAY republish with expiration set to the present, so a deliberate stop disappears at once rather than appearing stale.
Readers MUST ignore announcements dated meaningfully in the future, allowing for clock skew; created_at is publisher-chosen, so otherwise a compromised service could post-date one and appear alive indefinitely.
Readers MUST derive state from the age of the newest announcement, measured against that announcement's own interval:
- age < 2 ×
interval: running - age ≥ 2 ×
interval: not known to be running - no event: unknown, or expired
No status field is defined, and a self-reported one MUST NOT be trusted if present. A service that has crashed, lost power or been disconnected cannot publish its own failure, so the only reliable indicator of an outage is the absence of one.
Service Roster
Anyone can address an event to the owner, so a reader MUST NOT trust an announcement merely because it is addressed correctly.
{
"kind": 10200,
"pubkey": "<owner pubkey>",
"created_at": 1785399000,
"tags": [],
"content": "<NIP-44 ciphertext, owner to owner>"
}The plaintext is a JSON array of accepted service pubkeys:
["<service pubkey>", "<service pubkey>"]Readers MUST NOT trust announcements from keys absent from the roster, MUST consult it before trusting a key seen for the first time, and SHOULD surface absent keys as pending, so adding a service is an approval rather than an edit. Revocation is removal from the roster; because each service holds an independent key, one can be dropped without touching any other.
Reader behaviour
- Fetch the roster:
{"kinds": [10200], "authors": ["<owner pubkey>"]}. - Subscribe:
{"kinds": [30200], "#p": ["<owner pubkey>"]}. - Decrypt each
contentwith NIP-44, using the owner key and the event'spubkey; set aside events whose author is absent from the roster. - Keep the newest event per author and
d, and derive state as in Liveness.
Deployment
Nothing here depends on a particular network. A service need only open an outbound connection to a relay, and the reader reach the same relay, which is why no prober is needed and why services behind NAT require no special arrangement. The choice of network affects privacy and availability, not correctness.
Where services sit on a private network, announcements SHOULD go to a relay on it, which removes the third party entirely. Such a network need not be a LAN: an overlay that traverses NAT reaches a phone on a mobile network, so a private relay can still be read from anywhere. Owners MAY additionally mirror to a public relay so the inventory survives that network being degraded: the one case a private relay cannot cover.
The layering must not invert: the inventory is never how a reader finds that relay. An inventory that bootstraps itself cannot be read when it is most needed.
Privacy considerations
content is encrypted; the envelope is not. On a relay the owner does not control:
- The
ptag links every service to one owner, exposing the shape of a deployment: how many services, how often each reports. - The kind is itself a marker: anyone may subscribe to kind
30200and enumerate who uses this scheme without decrypting anything. No further marker tag is defined and none SHOULD be added, since it would only narrow that search. - The
dtag is public and MUST be opaque. A random identifier generated on first run suffices;hanami@box3would leak exactly what the encryption was for.
Announcements MAY instead be published as [NIP-59](59.md) gift wraps, so a relay cannot tell how many services exist or correlate one over time. Two rules change: the roster check applies to the seal's pubkey, since the wrapper's author is a throwaway key; and the future-timestamp check applies to the rumor's created_at, since NIP-59 randomises the wrapper's. The cost is that gift wraps are not addressable, so relays retain every announcement until expiry rather than only the latest, and owners choosing this SHOULD lengthen interval. It does not conceal that a recipient exists, as the p tag remains.
Security considerations
Announcements are hints, not authentication. A reader learns where a service claims to be; whether the thing answering is that service is decided by the transport (TLS, a key exchange, an onion address), never by this event. Implementations MUST NOT treat urls, host or name as grounds for trusting what they connect to. Where a network derives addresses from keys, the reachability half authenticates itself and readers SHOULD prefer it.
The roster is the trust anchor. Compromising one service key permits false statements about that service alone; introducing a new service requires the owner's key. metrics is a claim by a service about itself, and is exactly as trustworthy as that service.
A hostile or faulty relay fails safe: withholding announcements makes a service appear stale, and replaying an old one makes it appear staler still. Neither can make a stopped service appear to be running.
Scope
An inventory with liveness, deliberately not monitoring: no history, no aggregation, no alerting. metrics carries current gauges so a reader can show "94% full", and nothing further. Retention and alerting belong to monitoring software, which can announce itself here like anything else.