NIPs by PolleramaCommunity NIPs, surfaced by trustConnect
npub1uac67zc9er5...

[NIP-5D] Nostr Web Applets (napplets)

Published Jun 26, 2026
kind 5129 · napplet snapshotkind 15129 · root nappletkind 35129 · named napplet

NIP-5D

Nostr Web Applets

draft optional

This NIP defines a web projection for sandboxed web applications ("napplets") running in iframes to communicate with a hosting application ("shell"). Shells expose granted NAP (Nostr Applet Protocol) domains through an injected window.napplet namespace. Under that namespace, domain calls are carried over postMessage using a generic JSON envelope. Protocol messages are defined by NAP extension specs.

Philosophy

A napplet is a Nostr applet - a small, focused application that does one thing well. Napplets SHOULD be single-purpose rather than monolithic. A chat widget, a feed viewer, a profile editor, and a relay manager are four napplets, not one application with four tabs. The shell composes napplets; napplets do not compose themselves.

Terminology

TermDefinition
ShellWeb application hosting napplet iframes
NappletSandboxed iframe application communicating with the shell via postMessage
dTagNapplet identifier from the napplet manifest d tag
Aggregate hashA napplet's content address — the [NIP-5A](5A.md) aggregate hash of its path tags, carried in the manifest x tag
Napplet manifestNostr event (kind 5129 / 15129 / 35129) describing a napplet; tag schema adopted from [NIP-5A](5A.md)
NAPNostr Applet Protocol -- extension spec defining protocol messages for a capability domain

Transport

Communication uses an injected window.napplet namespace backed by postMessage. Napplet to shell: window.parent.postMessage(msg, '*'). Shell to napplet: iframeWindow.postMessage(msg, '*'). The '*' target origin is required because napplets have opaque origins (no allow-same-origin).

Napplet iframes are loaded via srcdoc (see [Identity](#identity)) and MUST use this sandbox attribute:

sandbox="allow-scripts"

The allow-same-origin token MUST NOT be present. Shells MAY add additional sandbox tokens (allow-forms, allow-modals, allow-downloads, allow-popups) based on shell policy. Napplets have no access to localStorage, sessionStorage, IndexedDB, direct WebSocket connections, or signing keys. All storage, signing, encryption, and relay access is proxied through the shell.

The shell identifies senders via MessageEvent.source (unforgeable Window reference). Messages from unknown sources (iframes not created by the shell) MUST be silently dropped.

Shells MUST inject window.napplet before any napplet script runs, including classic scripts, module scripts, reloads, and development wrappers. The namespace MUST contain only the NAP domain objects the shell exposes to that napplet. Presence of a domain object means that domain is available to the napplet. Absence means unavailable.

Shells MUST NOT provide window.nostr (NIP-07) to napplet iframes. Signing and encryption are security-critical operations that MUST be mediated by the shell. See the Security Rationale section below.

Wire Format

All messages between napplet and shell are JSON objects with a type field:

{ "type": "<domain>.<action>", ...payload }

The type field is a string discriminant in domain.action format. Domains correspond to NAP capability names (e.g., a NAP named foo owns all foo.* types). NAP specs define the valid type strings and payload shapes for their domain. This NIP does not enumerate message types.

Example — a hypothetical foo NAP with a request/response pattern:

{ "type": "foo.bar", "id": "abc", "data": {...} } { "type": "foo.bar.result", "id": "abc", "result": {...} }

Messages with an unrecognized type MUST be silently ignored. This allows forward compatibility as new NAPs are defined.

Identity

A napplet's identity is the (dTag, aggregateHash) tuple. The runtime computes it from the napplet's own bytes; it MUST NOT accept it from a host.

Before creating the iframe, the runtime MUST:

  1. Resolve the napplet manifest event (kind 5129, 15129, or 35129, see [Manifest](#manifest)) from relays and verify its signature.
  2. Fetch each path blob from Blossom by its sha256 and verify sha256(blob) matches the path hash.
  3. Recompute the aggregate hash from the path tags per [NIP-5A](5A.md); if the manifest carries an x tag it MUST match. This is aggregateHash.
  4. Inject the verified bytes via srcdoc (never a navigated src) and map the iframe's Window reference to (dTag, aggregateHash).

A gateway MAY serve the bytes as an accelerator or fallback, but the runtime MUST verify its output against the signed manifest — the gateway is never trusted.

Identity is thus fixed before any code runs and bound to the exact bytes that run. The shell MUST verify MessageEvent.source on every inbound message and silently drop messages from Window references not mapped to a napplet.

Manifest

A napplet is published as a napplet manifest event. Its tag schema is adopted from [NIP-5A](5A.md) — path, the x aggregate tag, server, and optional title / description / source — under NIP-5D's own kinds:

KindTyped tag
5129snapshot (regular)none
15129root napplet (replaceable)none
35129named napplet (addressable)identifier

Distinct kinds keep napplets out of nsite gateway resolution: a napplet is resolved and verified by the runtime ([Identity](#identity)), never served as an nsite.

A napplet is a single self-contained /index.html. The manifest MUST include a path tag per file — ["path", "/index.html", "<sha256>"] — and server tags SHOULD hint the Blossom servers holding those blobs. aggregateHash is the [NIP-5A](5A.md) aggregate hash of the path tags, carried in the x tag (["x", "<sha256-hex>", "aggregate"]). A 5129 snapshot is a regular event pinning a specific napplet version, per [NIP-5A](5A.md).

The manifest declares required capabilities with requires tags:

["requires", "<nap-name>"]

Each value is a bare NAP domain (e.g. relay, never NAP-RELAY). At load the shell checks requires against its capabilities; if one is absent it SHOULD reject the napplet or warn. With no requires tags it loads with whatever the shell provides.

Runtime Domain Availability

Napplets detect runtime domain availability from the injected namespace. If window.napplet.<domain> is present, the shell exposes that NAP domain to the napplet. If it is absent, the napplet MUST gracefully degrade or stop using that domain.

Domain object presence is only an availability signal. It does not define the domain's operations, payloads, error model, version support, or semantics. Those contracts remain in the matching NAP specs. If a domain needs semantic compatibility checks, version negotiation, or diagnostics, that domain MUST define them in its own NAP.

NAP Extension Framework

Protocol messages are defined by NAP (Nostr Applet Protocol) specs. Each NAP owns a message domain and defines the type strings, payload shapes, injected domain object behavior, and semantics for that domain. A NAP spec is self-contained — it references this NIP only for web namespace injection, envelope format, and transport.

For example, a NAP named foo would own all foo.* message types (e.g., foo.bar, foo.bar.result) and define their payloads and shell behavior.

NAP specs MUST:

  • Define all valid type strings for their domain
  • Specify the payload shape for each message type
  • Document expected shell behavior for each message
  • Be independently implementable — a shell MAY support any subset of NAPs

Security Considerations

Napplets are untrusted code. The shell is trusted. The browser enforces iframe sandbox boundaries. MessageEvent.source provides unforgeable sender identity.

Mitigations:

  1. Iframe sandbox: allow-scripts is the only required token -- shells MUST NOT add allow-same-origin. Adding allow-same-origin would grant the napplet a real origin, allowing it to register a service worker, read shell localStorage, and bypass shell mediation entirely -- this prohibition is the load-bearing precondition for browser-enforced isolation of any kind.
  2. postMessage '*' origin is required for opaque-origin iframes; sender identification uses MessageEvent.source, NOT event.origin.
  3. Identity binding: the runtime computes (dTag, aggregateHash) from the napplet's verified bytes before execution and maps it to the iframe Window. MessageEvent.source is unforgeable within the same browsing context.
  4. Content-addressed loading: the runtime verifies the manifest signature and each blob's sha256, then recomputes the aggregate from the path tags and asserts it equals any x tag (see [Identity](#identity)); a napplet failing any check MUST be rejected. Gateways are untrusted — their output is verified against the signed manifest.
  5. Runtime injection is outside the signed napplet artifact. It MUST be limited to the window.napplet namespace and MUST NOT change the napplet bytes used to compute aggregateHash.
  6. Unrecognized message types are silently ignored, preventing capability probing.
  7. Napplets produce cleartext only. Shells MUST NOT sign or broadcast events containing ciphertext received from a napplet. Shells MUST NOT provide window.nostr (NIP-07) or any signing/encryption primitives.

Storage isolation, relay access control, and ACL enforcement are defined by their respective NAP specs.

Non-Guarantees: The protocol does NOT protect against a compromised browser, a malicious shell, side-channel attacks, or social engineering.

References

  • [NIP-5A](5A.md) -- manifest tag schema adopted by the napplet manifest
  • NAPs -- NAP domain and message-protocol registry