---
name: port-nostr-app
description: Use when converting an existing Nostr web app into a napplet - audit app-owned relay, signing, storage, network, media, keyboard shortcuts, and routing layers; map each feature to a current package-implemented NAP boundary; default social reads and publishes to NAP-OUTBOX, not NAP-RELAY; then hand a clean build spec to design-napplet/build-napplet.
---

# Porting A Nostr App To A Napplet

Use this before `design-napplet` when the starting point is an existing Nostr app. Do not copy the app architecture into the iframe. A napplet is sandboxed UI plus intent; the shell owns keys, signing, encryption, relay routing, storage, network bytes, and policy.

Protocol truth: NIP-5D (<https://github.com/nostr-protocol/nips/pull/2303>) plus NAPs (<https://github.com/napplet/naps>). Open the canonical spec before adding or depending on any message type, manifest tag, capability, or loading rule. If a needed surface is undefined, flag the gap instead of inventing it.

Use current package exports as the implementation boundary. Implemented package
domains are `relay`, `identity`, `storage`, `inc`, `theme`, `keys`, `media`,
`notify`, `config`, `resource`, `cvm`, `outbox`, `upload`, `intent`, `ble`,
`webrtc`, `link`, `count`, `lists`, `serial`, `common`, and `dm`. If a NAP is
not exported by the current `@napplet/*` packages, do not treat it as usable API.
Map a need to an existing shipped NAP only when it faithfully owns the intent;
otherwise list the feature as blocked/deferred.

Ports should replace app-owned infrastructure with `@napplet/sdk` imports, not
with hand-written `window.napplet.<domain>.*` clients. Keep direct
`window.napplet?.domain` access for post-injection optional-domain fallback
checks; use SDK helper exports for actual calls whenever they exist.

Current packages do not expose `window.napplet.shell`, `shell.ready()`, or
`shell.supports(...)`. Use `@napplet/sdk` wrappers for calls and use
`window.napplet?.domain` only as an optional-domain fallback check after runtime
injection. If a source app needed an async service-discovery layer, remove that
layer or flag the missing package/runtime surface instead of recreating it
inside the napplet.

## Sandbox Authority Contract

Do not preserve browser authority from the source app. A port is not complete
until these surfaces are gone from napplet runtime code:

- `fetch`, `XMLHttpRequest`, `WebSocket`, relay pools, direct NIP-65 routing, and
  any app-owned network fanout.
- `localStorage`, `sessionStorage`, IndexedDB, cookies, filesystem-like browser
  caches, and ad hoc persistence.
- External `<script src>`, `<link href>`, `<img src>`, `<audio src>`,
  `<video src>`, CSS network URLs, and dynamic network imports.
- `window.nostr`, raw private keys, local signing, local encryption/decryption,
  or extension-specific signer assumptions.

For resource-heavy ports, inventory every ROM, WASM side file, avatar, image,
font, media file, and JSON fetch. Bundle immutable bytes into the single-file
artifact when that is the product shape; otherwise route them through
`resource.bytes` / `resource.bytesMany`. If the old dependency cannot run
without direct browser fetch/storage/socket authority, stop and flag that
dependency instead of wrapping it in a napplet.

## Migration Rule

Replace app-owned infrastructure with the highest-level NAP that owns the user intent:

| Existing app code | Napplet boundary |
| --- | --- |
| Relay pools, NIP-65 outbox resolution, relay ranking, fanout, dedup, event validation | `outbox` |
| One explicit relay, group relay, raw relay diagnostics, protocol tooling outside the outbox model | `relay` |
| `window.nostr`, private keys, nostr-tools signing, NIP-07 assumptions | Shell-mediated publish/action NAPs (`outbox`, `common`, `lists`, `dm`) |
| Follow/unfollow/react/report/profile lookup/NIP-19 helpers | `common` |
| NIP-51/NIP-65 list read-modify-write | `lists` |
| Reaction/reply/repost/quote/report/follower counts | `count` |
| DM protocol, message storage, encryption, key sessions, relay routing | `dm` |
| `localStorage`, IndexedDB, cookies | `storage` |
| `fetch`, `XMLHttpRequest`, WebSocket, direct external images/media | `resource`, `upload`, `link`, `media`, or shell-owned NAPs |
| App-level cross-window/plugin bus | `inc` or `intent` |
| Global shortcuts, editor hotkeys, action keybindings | `keys` |
| Notifications, badges, notification actions | `notify` |
| Device/native session bridges | `ble`, `serial`, `webrtc`, or `cvm` only when the current package domain fits |

If the port still contains a relay client, signer, direct storage layer, or direct network layer after this pass, assume the boundary is wrong until proven otherwise.

Do not add `keys` to `requires` when local buttons, menus, text input, or
click/tap controls let the napplet function without shell-managed key
reservation. Treat key reservation as optional unless the port's core workflow
cannot work without shell-owned reserved shortcuts.

## Step 1 - Inventory The Existing App

Make a table before editing:

```
feature | existing code path | current direct authority | replacement NAP | hard/optional | notes
feed reads | RelayPool.subscribe + NIP-65 | relay routing | outbox | hard | author write relays matter
post note | signer.sign + relay.publish | signing + fanout | outbox.publish | hard | toInboxes for directed posts
react | build kind 7 + signer + relay | social action | common.react | optional | fallback disables action
mute pubkey | edit kind 10000 | list mutation | lists.add | optional | preserve list state shell-side
avatar | fetch(url) | network bytes | resource.bytes | optional | object URL + revoke
```

Use this table as the design spec input. Do not start by wiring compatibility shims around the old app services.

## Step 2 - Split Monoliths Into Napplets

NIP-5D napplets should be small and focused. A full Nostr app usually becomes several napplets:

| App area | Likely napplet |
| --- | --- |
| Home/feed timeline | feed viewer |
| Composer | note composer |
| Profile page/editor | profile viewer/editor |
| Notifications | notification viewer |
| Direct messages | DM client |
| Relay/admin tools | relay diagnostic or relay manager |

Use `inc` or `intent` for handoff between napplets. Do not recreate the monolith with tabs unless the prompt explicitly asks for a single compound app and the shell context supports that shape.

## Step 3 - Rewrite Data Access

Default patterns:

```ts
import { outbox, common, lists, count, dm, storage, resource } from '@napplet/sdk';

const { events } = await outbox.query(
  [{ authors: [author], kinds: [1], limit: 20 }],
  { authors: [author], timeoutMs: 3000 },
);

const sub = outbox.subscribe([{ kinds: [1], authors: [author], limit: 50 }], {
  authors: [author],
  timeoutMs: 3000,
});
sub.on('event', (result) => render(result.event, result.sidecar?.relayHints));

const publish = await outbox.publish({
  kind: 1,
  content,
  tags,
  created_at: Math.floor(Date.now() / 1000),
});
if (!publish.ok) showPublishError(publish.error);

await common.react(noteId, '+');
await lists.add({ type: 'mute-list' }, [{ itemType: 'pubkey', value: pubkey }]);
const totals = await count.query({ kinds: [7], '#e': [noteId] });
await dm.send({ recipients: [pubkey], content: 'hello' });
```

Only keep `relay` when the inventory table says "explicit relay-local escape hatch" and names the reason.

## Step 4 - Remove Forbidden Surfaces

Search and delete or replace:

- `window.nostr`, private key material, `signEvent`, `nip04`, `nip44`, local encryption/session keys.
- `new WebSocket("wss://...")`, relay pool libraries, direct NIP-65 resolvers, app-owned fanout/dedup.
- `fetch`, `XMLHttpRequest`, direct `<img src="https://...">`, external scripts/styles.
- `localStorage`, `sessionStorage`, IndexedDB, cookies.
- Generic capability probes such as `discoverServices`, `hasService`,
  `shell.ready()`, or `shell.supports(...)`.

Optional-domain fallback checks are property checks after runtime injection; use
SDK imports for the actual calls inside enabled branches:

```ts
if (window.napplet?.common) enableSocialActions();
else disableSocialActions();
```

## Step 5 - Hand Off To Build

Produce this handoff for `design-napplet` / `build-napplet`:

```
nappletType:
source app:
features kept:
features split/deferred:
NAPs used:
package/proposal gaps:
requires:
optional domains and fallbacks:
SDK helpers/imports:
outbox reads:
outbox publishes:
social/list/count/dm actions:
keys/media/notify/device domains:
relay escape hatches:
storage keys:
resource URLs/schemes:
removed app-owned infrastructure:
verification scenarios:
```

The stop condition for this skill is not "the old app compiles." It is "the napplet no longer owns authority that belongs to the shell, and every feature maps to a current NIP-5D/NAP/package surface."
