Appearance
Tutorial: Minimal Host Shell
This tutorial shows the smallest shape of a browser host that embeds one sandboxed napplet through Kehto.
Alpha status: Kehto is an early runtime implementation for a draft NIP-5D protocol. Use this tutorial as current implementation guidance; NAP contracts, capability names, and helper APIs may change.
1. Install the runtime packages
bash
pnpm add @kehto/runtime @kehto/shell @kehto/services @napplet/core @napplet/nap nostr-toolsUse @kehto/runtime for the protocol engine, @kehto/shell for browser iframe/message integration, and @kehto/services for reference service handlers.
2. Build host adapters
The shell bridge needs host-owned hooks. A minimal host can begin with no-op or in-memory implementations, then replace them with real relay, signer, cache, and persistence backends.
ts
import { createShellBridge, type ShellAdapter } from '@kehto/shell';
const adapter: ShellAdapter = {
relayPool: {
getRelayPool: () => null,
trackSubscription: () => {},
untrackSubscription: () => {},
openScopedRelay: () => {},
closeScopedRelay: () => {},
publishToScopedRelay: () => false,
selectRelayTier: () => [],
},
relayConfig: {
addRelay: () => {},
removeRelay: () => {},
getRelayConfig: () => ({ discovery: [], super: [], outbox: [] }),
getNip66Suggestions: () => [],
},
windowManager: {
createWindow: () => null,
},
auth: {
getUserPubkey: () => null,
getSigner: () => null,
},
config: {
getNappUpdateBehavior: () => 'banner',
},
hotkey: {
executeHotkeyFromForward: () => {},
},
workerRelay: {
getWorkerRelay: () => null,
},
crypto: {
verifyEvent: async () => false,
},
dm: {
sendDm: async () => ({ success: false, error: 'not configured' }),
},
};
const bridge = createShellBridge(adapter);
window.addEventListener('message', bridge.handleMessage);3. Register reference services
The bridge exposes the underlying runtime. Register service handlers before loading napplet iframes.
ts
import { createNotifyService, createThemeService } from '@kehto/services';
bridge.runtime.registerService('notify', createNotifyService());
bridge.runtime.registerService('theme', createThemeService({
getTheme: () => ({ colors: {}, title: 'Default' }),
}));Use real host callbacks as you move beyond the minimal shell.
4. Load one sandboxed napplet
Use the same security posture as the playground: opaque-origin iframe, scripts only, no same-origin.
ts
const iframe = document.createElement('iframe');
iframe.sandbox.add('allow-scripts');
iframe.src = '/napplet-gateway/example-dtag/example-hash/index.html';
document.body.append(iframe);Before marking the napplet usable, register the session identity from the gateway metadata and manifest. In the playground this is handled by the shell-host gateway path; host apps should keep the same ordering:
- Fetch manifest metadata.
- Resolve
(dTag, aggregateHash). - Register session identity.
- Navigate the iframe to the gateway artifact.
For repeated loads, add the optional NIP-5D artifact cache during the resolve step. The cache reuses verified bytes only; it does not replace manifest, aggregate, or blob-hash verification. See Implement a napplet artifact cache.
5. Tear down cleanly
ts
window.removeEventListener('message', bridge.handleMessage);
bridge.destroy();Destroying the bridge clears runtime subscriptions, buffers, and registries. Host-owned relay pools, timers, and native bridges should also be torn down in the same lifecycle.