[← All NAPs](https://napplet.run/protocol/)

NAP-WEBRTC / PR #59

# Runtime-Mediated WebRTC Signaling

Open PR

This is an unmerged proposal. Its API and scope may change during discussion. Participate on the pull request before depending on it.

**Shipped implementation: experimental**

Signaling and session binding have unit coverage; interoperable live peer sessions remain unverified here.

Implementation proposal: [in-review ↗](https://github.com/napplet/naps/pull/59), checked 2026-10-31. This assessment does not imply that every amendment shown here is implemented. All capabilities remain subject to change.

[Maturity and opt-in policy →](https://napplet.run/docs/naps/maturity.html)

Full specification from [the fetched upstream revision ↗](https://github.com/napplet/naps/blob/69523e8cf44c8653155c5e77d25340d4ea3d0887/naps/NAP-WEBRTC.md). The document below is rendered from its original Markdown. [Follow the latest source on GitHub ↗](https://github.com/napplet/naps/pull/59/files).

## NAP-WEBRTC

### Runtime-Mediated WebRTC Signaling

`draft`

**NAP ID:** NAP-WEBRTC **Domain:** `webrtc` **Depends:** none. **Web binding ([NIP-5D](https://github.com/nostr-protocol/nips/pull/2303)):** `window.napplet.webrtc`; domain presence signals availability.

### Description

NAP-WEBRTC lets a napplet ask the shell to establish a WebRTC peer session whose signaling is carried over Nostr. The shell owns the signaling mechanism, relay subscriptions, signing, encryption, SDP offers and answers, ICE candidates, and `RTCPeerConnection` lifecycle. The napplet names the peer or room it wants to join and sends or receives opaque application payloads once the session is open.

This is a transport capability, not a call protocol. It does not standardize voice, video, mute state, chat messages, or application commands. Those semantics belong in a NAP-N or an application protocol carried over the established channel.

### API Surface

| Operation | Parameters | Result | Wire |
| --- | --- | --- | --- |
| `open` | `request` (`WebrtcOpenRequest`) | `WebrtcOpenResult` | `webrtc.open` / `webrtc.open.result` |
| `send` | `sessionId` (`tstr`), `payload` (`any`) | none | `webrtc.send` / `webrtc.send.result` |
| `close` | `sessionId` (`tstr`), optional `reason` (`tstr`) | none | `webrtc.close` / `webrtc.close.result` |
| `onEvent` | handler for `WebrtcEvent` | `Subscription` handle | `webrtc.event` |

#### Schemas

Enums:

| Name | Values |
| --- | --- |
| `WebrtcState` | `connecting`, `open`, `closed` |
| `WebrtcPeerState` | `joined`, `left` |

`WebrtcScope` variants:

| Variant | Required fields | Optional fields |
| --- | --- | --- |
| direct | `type: "direct"`, `pubkey: tstr` | none |
| room | `type: "room"`, `room: tstr` | `peers: list of tstr` |

`WebrtcOpenRequest` fields:

| Field | Required | Type |
| --- | --- | --- |
| `scope` | yes | `WebrtcScope` |
| `channel` | no | text |
| `protocol` | no | text |

`WebrtcOpenResult` fields:

| Field | Required | Type |
| --- | --- | --- |
| `session` | yes | `WebrtcSession` |

`WebrtcSession` fields:

| Field | Required | Type |
| --- | --- | --- |
| `id` | yes | text |
| `scope` | yes | `WebrtcScope` |
| `channel` | yes | text |
| `protocol` | no | text |
| `state` | yes | `WebrtcState` |

`WebrtcEvent` variants:

| Variant | Required fields | Optional fields |
| --- | --- | --- |
| state | `type: "state"`, `sessionId: tstr`, `state: WebrtcState` | none |
| peer | `type: "peer"`, `sessionId: tstr`, `pubkey: tstr`, `state: WebrtcPeerState` | none |
| message | `type: "message"`, `sessionId: tstr`, `from: tstr`, `payload: any` | none |
| closed | `type: "closed"`, `sessionId: tstr` | `reason: tstr` |

**`open(request)`** — Requests a WebRTC session. For `direct`, the shell connects to one peer pubkey. For `room`, the shell announces presence to the room and may connect to discovered peers or to the requested `peers` subset. `channel` names the WebRTC data channel; when omitted, the shell chooses a default. `protocol` is an application-level label for the payloads carried after connection and has no NAP-WEBRTC semantics.

**`send(sessionId, payload)`** — Sends an opaque payload over the established session. The shell serializes and delivers it over the WebRTC data channel. This method does not publish the payload to Nostr.

**`close(sessionId, reason?)`** — Closes the session and asks the shell to publish any corresponding disconnect signal where appropriate.

**`onEvent(handler)`** — Registers for shell-pushed session events. Message payloads are untrusted input from peers.

### Wire Protocol

`webrtc.*` messages use the [NIP-5D](https://github.com/nostr-protocol/nips/pull/2303) wire format (`{ "type": "domain.action", ...payload }`).

| Type | Direction | Payload fields |
| --- | --- | --- |
| `webrtc.open` | napplet -> shell | `id`, `request` |
| `webrtc.open.result` | shell -> napplet | `id`, `session`?, `error`? |
| `webrtc.send` | napplet -> shell | `id`, `sessionId`, `payload` |
| `webrtc.send.result` | shell -> napplet | `id`, `error`? |
| `webrtc.close` | napplet -> shell | `id`, `sessionId`, `reason`? |
| `webrtc.close.result` | shell -> napplet | `id`, `error`? |
| `webrtc.event` | shell -> napplet | `event` |

Key design notes:

-   Request/result pairs use `id` for correlation.
-   `webrtc.event` has no `id`; it is shell-initiated delivery.
-   Napplet-facing messages never carry raw SDP, ICE candidates, or raw signaling payloads.
-   The shell may deliver `webrtc.event` state updates before `open()` resolves if the underlying connection progresses quickly, but it MUST NOT deliver messages for an unknown `sessionId`.

#### Examples

**Open a direct peer channel:**

```
-> {
     "type": "webrtc.open",
     "id": "w1",
     "request": {
       "scope": { "type": "direct", "pubkey": "abc123..." },
       "channel": "chat",
       "protocol": "chat:live"
     }
   }
<- {
     "type": "webrtc.open.result",
     "id": "w1",
     "session": {
       "id": "sess-1",
       "scope": { "type": "direct", "pubkey": "abc123..." },
       "channel": "chat",
       "protocol": "chat:live",
       "state": "connecting"
     }
   }
<- { "type": "webrtc.event", "event": { "type": "state", "sessionId": "sess-1", "state": "open" } }
```

**Send an application payload:**

```
-> {
     "type": "webrtc.send",
     "id": "s1",
     "sessionId": "sess-1",
     "payload": { "body": "hello" }
   }
<- { "type": "webrtc.send.result", "id": "s1" }
```

**Receive a peer payload:**

```
<- {
     "type": "webrtc.event",
     "event": {
       "type": "message",
       "sessionId": "sess-1",
       "from": "abc123...",
       "payload": { "body": "hi" }
     }
   }
```

### Signaling

NIP-100 is an unmerged draft at [https://github.com/nostr-protocol/nips/pull/363](https://github.com/nostr-protocol/nips/pull/363). It is a useful reference for WebRTC signaling over Nostr, but it is not part of the NAP-WEBRTC interface.

A conformant shell MAY use NIP-100, a later accepted Nostr signaling NIP, or another Nostr signaling scheme. That choice is a runtime detail. The napplet observes only the `webrtc.*` interface, session state, peer state, and opaque peer messages defined here.

Regardless of signaling scheme, the shell MUST treat raw signaling payloads as shell-internal transport data, not as napplet-visible API data.

### Error Handling

Any result message MAY include an `error` field (string). When `error` is present, other result fields are undefined.

Common errors include `"unsupported scope"`, `"signaling unavailable"`, `"peer unavailable"`, `"session not found"`, `"session closed"`, `"payload too large"`, and `"policy denied"`.

The shell SHOULD send a `webrtc.event` with `type: "closed"` when an established session ends because a peer disconnects, ICE fails, signaling expires, or policy revokes the capability.

### Shell Behavior

-   The shell MUST respond to every request with a result message carrying the same `id`.
-   The shell MUST NOT expose raw relay sockets, signing keys, SDP descriptions, ICE candidates, or `RTCPeerConnection` objects to napplets through this NAP.
-   The shell MUST perform any signaling signing and encryption itself. It MUST NOT delegate keys or raw signing material to the napplet.
-   The shell MUST scope sessions per napplet and MUST NOT deliver one napplet's peer messages to another napplet.
-   The shell MUST enforce per-napplet policy before opening a session, publishing signaling events, or delivering peer messages.
-   The shell MAY limit relays, peers, rooms, payload sizes, data-channel labels, connection duration, or concurrent session count.
-   The shell MAY route a room session through a WebRTC media server or peer mesh. That topology is shell policy and is not visible to the napplet.
-   The shell SHOULD publish `disconnect` when `close()` succeeds or when it can gracefully tear down a session.

### Security Considerations

-   WebRTC can reveal network metadata to peers. Shells MUST treat `open()` as an untrusted request and SHOULD require user policy or consent before starting a session.
-   Signaling room identifiers can be sensitive. Shells SHOULD prefer random, non-meaningful room ids and SHOULD avoid exposing private room ids to napplets that did not create or join them.
-   Peer payloads are untrusted. Napplets MUST validate every `message` payload against their own application protocol before acting on it.
-   The shell owns encryption and signing. Napplets MUST NOT receive private keys, raw signaling payloads, or decrypted signaling secrets through this interface.
-   The shell SHOULD rate-limit `open()` and `send()` to prevent relay spam, connection floods, and peer abuse.
-   A runtime that supports media streams through a future extension MUST keep camera, microphone, and screen capture under user control. NAP-WEBRTC alone does not grant media capture authority.

### Implementations

-   (none yet)

### Changelog

-   `1a83117` - Introduced NAP-WEBRTC as a runtime-owned WebRTC signaling surface.
    
-   `394b597` - Adopted injected-domain availability and linked the current upstream web binding.
    

[Join the discussion · PR #59 ↗](https://github.com/napplet/naps/pull/59)[Read proposed changes ↗](https://github.com/napplet/naps/pull/59/files)

On this pageSpecification metadata

Capability domain

`webrtc`

Repository state

Open PR

Registry status

Not listed in the registry

Document marker

draft

Declared dependencies

None declared in the preamble

Source revision

`69523e8cf44c`

[View source on GitHub ↗](https://github.com/napplet/naps/pull/59/files)
