# Webxdc apps in posts

Status: phase 1 built (desktop verified headless; Android compiles, not yet run on a device).

## What a post carries

The NIP-DC draft ([nostr-protocol/nips#2230](https://github.com/nostr-protocol/nips/pull/2230))
shares a webxdc app (a `.xdc` zip of a sandboxed HTML5 app) as a URL with
`m application/x-webxdc`: in a kind-1 note's `imeta`, or a kind-1063 file event. It adds `x`
(the archive's sha256) and `webxdc` (an instance id: posts that share it share the app's state).
Real posts also come as a bare `.xdc` link with no `imeta`, and as an `imeta` with only `m`.

`WebxdcApp` reads all of these off the post's own tags; `RichTextParser` turns such a link into a
`WebxdcSegment`, and `FileMetadataAttachmentCard` does the same for a 1063. Both draw `WebxdcCard`:
name, size, "unverified" when there is no hash, and **Start**.

## Running one

A third host posture, `HostProfile.WEBXDC`, beside NAPPLET and WEBSITE:

| | NAPPLET | WEBSITE | WEBXDC |
|---|---|---|---|
| Origin | opaque (`srcdoc`) | its own, per account | its own, per account and app instance |
| Storage | none | persistent | persistent (webxdc requires it) |
| Network | none | yes (Tor or open web) | none (webxdc requires it) |
| WebRTC | removed | kept | removed |
| NIP-07 / broker | declared NAPs | NIP-07 | nothing |

The origin is the poster's app instance (`NappletLaunch.fromWebxdc`): another author reusing the
same `webxdc` id gets an origin, and storage, of its own.

`WebxdcBundles` downloads the archive (streamed to disk and hashed on the way, at most 256 MB; the
largest published is a 178 MB Half-Life), checks `x`, and unpacks it under limits (1 GB inflated,
20,000 entries, nothing outside its directory) into a directory keyed by hash, renamed into place
whole. `WebxdcFiles` serves it through the shared `NappletSiteServer` with the messenger's
`webxdc.js` (`WebxdcRuntime`) at the root.

## Phase 1: updates stay on the device

`window.webxdc` is complete except for transport: `sendUpdate` appends to a log in the app's own
IndexedDB and `setUpdateListener` replays it by serial, so an app's state survives closing it or
killing the app. `selfAddr` is a pseudonym, `sha256("webxdc-addr:<poster>:<instance>:<pubkey>")`
cut to 40 hex characters: stable, the same for anyone who computes it from an update's author, and
different in every app, so two apps can't match players by it (NIP-DC lets it be any value). It is not secret: it has no secret input, so anyone who knows the poster and instance can hash the pubkeys they know and find the player's (2026-10-31 security review).
`selfName` is the display name. The app can't open links either: no network means no way out.
`importFiles` uses a file input; `sendToChat` rejects; `joinRealtimeChannel` returns a channel that
reaches no one.

## Next

1. **Updates over Nostr (NIP-DC kind 4932):** publish each `sendUpdate` as a signed event tagged
   `i` (one consent per app: "publishes public updates as you"), subscribe `#i`, order by
   `created_at` then id, keep a local copy so the app opens offline. Relays: the post's, the
   poster's outbox, and the user's outbox. The draft doesn't say; this is a proposal.
2. **Realtime (kind 20932, ephemeral)** for `joinRealtimeChannel`.
3. `sendToChat` (a note with the file, after the user confirms), the card's last
   `summary`/`document` from the latest update, the app's icon and name from `manifest.toml`.
4. Concord (kind 3310, held by the session already) and NIP-29 group (9450/24450) transports.
5. Trimming the unpacked bundles directory.
