Appearance
Tutorial: Runtime Implementation
This guide expands the minimal host into the normal implementation sequence for a browser shell.
Step 1: Define host responsibilities
Kehto owns protocol dispatch and shell/runtime contracts. Your host owns:
- iframe creation and layout;
- relay pool selection and publication;
- signer access;
- persistence storage;
- capability grants and revocations;
- service backing callbacks;
- user-facing consent and compatibility warnings.
Step 2: Create the shell bridge
Use createShellBridge() when you are in a browser. It creates a runtime internally and adapts browser hooks to runtime adapters.
ts
import { createShellBridge } from '@kehto/shell';
const bridge = createShellBridge(adapter);
window.addEventListener('message', bridge.handleMessage);Use createRuntime() directly only for non-browser tests, alternative transports, or host shells that do not use the Kehto browser bridge.
Step 3: Register services
Register each service under the domain name that incoming NAP messages use.
ts
import {
createIdentityService,
createRelayPoolService,
createKeysService,
createMediaService,
createNotifyService,
} from '@kehto/services';
bridge.runtime.registerService('identity', createIdentityService(identityOptions));
bridge.runtime.registerService('relay', createRelayPoolService(relayOptions));
bridge.runtime.registerService('keys', createKeysService(keysOptions));
bridge.runtime.registerService('media', createMediaService(mediaOptions));
bridge.runtime.registerService('notify', createNotifyService(notifyOptions));Keep host-specific behavior behind options and bridge interfaces. Do not patch message dispatch.
Step 4: Define ACL policy
The runtime checks every capability-gated message. Grant only the capabilities the napplet manifest requires and the user has accepted.
ts
bridge.runtime.aclState.grant(pubkey, dTag, aggregateHash, 'relay:write');
bridge.runtime.aclState.grant(pubkey, dTag, aggregateHash, 'notify:send');Use the package docs for @kehto/acl when you need lower-level state migration or policy tests.
Step 5: Load gateway artifacts
Follow the playground order. Hosts that repeatedly load the same napplets can open the optional artifact cache before resolving the manifest:
ts
import { openNappletArtifactCache, resolveNapplet } from '@kehto/nip/5d';
const cache = await openNappletArtifactCache({ requireStorageEstimate: true });
const resolved = await resolveNapplet({ event, fetchBlob, cache });The cache is only a reuse layer for verified blobs and aggregate metadata. resolveNapplet() still verifies the manifest signature, recomputes the aggregate, and re-hashes every cached blob before rendering.
Then continue the load sequence:
- Read
/napplet-gateway/<dTag>/manifest.json. - Parse
requirestags. - Compare required NAPs against hosted
shell.supports()capability inventory. - Register
(dTag, aggregateHash)identity before iframe navigation. - Navigate to
/napplet-gateway/<dTag>/<aggregateHash>/index.html.
Reject or warn before loading when a required capability is unsupported. For cache setup details, see Implement a napplet artifact cache.
Step 6: Handle teardown
On shell shutdown:
- Remove
messagelisteners. - Destroy
ShellBridge. - Stop host relay subscriptions.
- Unsubscribe native key/media bridges.
- Persist or clear host-owned state according to your product policy.
Do not leave runtime sessions or relay subscriptions alive after the iframe is removed.