Skip to content

Architecture ​

kernl is three layers. kehto supplies the primitives, four kernl packages supply the opinions, and one app renders them. Nothing in a lower layer knows about a higher one.

┌──────────────────────────────────────────────────────────────────────┐
│  apps/web  (@kernlize/web)                        Astro 7 + Svelte 5    │
│  landing site at /         shell at /app/      docs at /docs/        │
├──────────────────────────────────────────────────────────────────────┤
│  @kernlize/core                                   composition root      │
│  createKernl · applesauce adapters · decodePointer → resolve →       │
│  plan → mount · guard pipeline · grants · describeChange · layouts   │
│       │                 │                   │                        │
│  @kernlize/guard      @kernlize/messaging     @kernlize/bridges               │
│  ingress, gate,    provider contract,   locators, Bridge contract,   │
│  focus firewall,   registry, composite  registry, HTTP bridge        │
│  CSP, profiles     dm adapter           with SSRF policy             │
├──────────────────────────────────────────────────────────────────────┤
│  kehto   @kehto/shell · runtime · acl · firewall · services · nip ·  │
│          wm                                                          │
│  applesauce   EventStore · RelayPool · AccountManager · signers ·    │
│               loaders                                                │
├──────────────────────────────────────────────────────────────────────┤
│  @napplet/core   NAP wire types, NapDomain, envelope dispatch        │
└──────────────────────────────────────────────────────────────────────┘

The kehto layer ​

kehto is the runtime authority for how a NIP-5D shell composes. kernl uses these seams unchanged:

kehto packageWhat kernl takes from it
@kehto/shellcreateShellBridge, buildShellCapabilities, resolveShellEnvironment, prepareNappletSrcdoc, denyNappletControllerAccess, renderNappletFramePolicyMeta, originRegistry, the ShellAdapter hooks (including filterNappletMessage, getFocusContext, onFirewallEvent, firewallPersistence and aclDefaultPolicy), ShellBridge.releaseDeferred and the revocation model.
@kehto/runtimeThe protocol engine behind the bridge: dispatch, the ACL gate (bridge.runtime.aclState), the runtime firewall (bridge.runtime.firewallState, with setUnfocusedMultiplier and setBurstGuard), the ServiceRegistry and ConsentRequest.
@kehto/aclThe grant store the runtime enforces, keyed by dTag:hash. kernl reaches it only through bridge.runtime.aclState.
@kehto/firewallThe pure evaluate() engine @kernlize/guard runs, and the NappletRules and RateLimit types runtimeFirewallRules translates into.
@kehto/servicescreateRelayPoolService, createIdentityService, createThemeService, createNotifyService, createLinkService, createResourceService, createDmService, createOutboxService and createRelayPoolOutboxRouter.
@kehto/nipresolveNapplet, openNappletArtifactCache and NAPPLET_KINDS from @kehto/nip/5d.
@kehto/wmLayoutStrategy, WindowState, Rect and WindowPlacement, which the layouts implement.

kernl never re-implements a kehto primitive. When a seam is too narrow, kernl composes in front of it and takes the gap upstream. The four gaps found first, filed as kehto/web #397–#400, are released in @kehto/shell 0.32.0 and in use (D-020); what is still open is in Alpha status.

One event layer ​

Every relay connection, event store, signer, account and loader goes through applesauce v7 (D-002). createKernl creates exactly one of each per shell, or uses the one the host passes in:

InstanceRoleAdapted by
EventStoreThe in-memory, reactive store of every event the shell knows. Applets never receive a reference; createIdentityProviders (applesauce/identity-providers.ts) and lookupReplaceable read from it on their behalf.kehto's identity service and the consent handler.
RelayPoolEvery relay connection.createRelayPoolHooks and createRelayPoolLike (applesauce/relay-hooks.ts) for ShellAdapter.relayPool; createAppletRelayBackend, createDmRelayPool and createOutboxRelayPool (applesauce/relay-backend.ts) for the relay, dm and outbox services. All of them route through selectRelayTier.
AccountManagerThe signed-in accounts and their signers.createKernlAccounts (applesauce/accounts.ts) refuses plaintext-key account types; createAuthHooks and wrapAccountSigner (applesauce/auth-hooks.ts) give kehto a signer that never reveals the account.

nostr-tools is a peer dependency of @kernlize/core, because kehto and applesauce require it. kernl source never imports it, and scripts/check-dependency-rules.mjs fails the build if it does.

The kernl packages ​

PackageDepends onPurpose
@kernlize/guard@kehto/firewallFlood control, visibility gate, focus-aware firewall, CSP profile, mitigation descriptions. Pure: no DOM, no timers, time is injected.
@kernlize/messaging@napplet/core (types)The provider contract and the composite backend. Structurally identical to kehto's DmAdapter so a composite can be handed to createDmService without translation, while the package stays free of a kehto dependency.
@kernlize/bridgesnothingLocator parsing, the Bridge contract, the registry with its authorize hook, the HTTP bridge.
@kernlize/coreall of the above, kehto, applesauceComposition: adapters, the applet lifecycle, the guard pipeline and runtime firewall rules, grants, consent summaries, layouts, the namespaced store. Browser-agnostic; the DOM is reached through KernlHost and FrameLike.

The three leaf packages have zero framework dependencies and import nothing from each other. Only core composes them. The dependency rules are enforced by pnpm check:deps.

Where each NAP domain is served from ​

An applet reaches the shell through the NAP domains in window.napplet. createKernlServices (kernl-services.ts) registers eight kehto services; shell, inc, storage, keys and media are routed by kehto's runtime itself. What one applet sees is buildShellCapabilities(adapter).domains narrowed by createEnvironmentResolver(grants).

NAP domainServed bykernl suppliesGuarded by
shell, inckehto runtimethe environment from resolveShellEnvironment, narrowed per identityidentity binding; baseline, no grant needed
storagekehto runtimethe ACL state quota (grants.setStateQuota)baseline; rate storage:write
identitycreateIdentityServicethe active signer through createAuthHooks; profile, follows, mutes and relays from createIdentityProvidersgrant (identity:read), "your account" in the shell
themecreateThemeServiceKernlOptions.theme when isTheme accepts itbaseline
relaycreateRelayPoolServicecreateAppletRelayBackend over the one pool, routed by selectRelayTiergrant; rates relay:subscribe, relay:publish; subscribe envelope cap
outboxcreateOutboxServicecreateRelayPoolOutboxRouter over createOutboxRelayPool, relay lists from createIdentityProviders, signing through the active signer (denied:read-only without one)grant; rates outbox:subscribe, outbox:publish; consent for destructive kinds
resourcecreateResourceServicecreateBridgeResourceResolver(bridges) with the requesting applet named through resourceInitForgrant; rate resource:bytes, resource envelope cap, hidden resource allowance, SSRF policy
dmcreateDmServicemessaging.composite()grant; rate dm:send; provider availability
notifycreateNotifyServicehost.notify with tone infogrant
linkcreateLinkServicehost.openLink after LINK_PROTOCOLS (https:, mailto:) allowed itgrant
intentcreateIntentService with kernl's router (createKernlIntents)the installed set, the user's defaults and published preferences, host.chooseHandler, host.openApplet; delivery over the target's bound portgrant (intent:read, intent:write); rate intent:invoke; see The store and intents
keys, mediakehto runtimenothing: hotkeys are not forwarded and no media hook is wiredgrant

So the shell offers fourteen domains: the baseline shell, inc, theme and storage, and identity, relay, outbox, resource, dm, notify, link, intent, keys and media behind a grant. Domains kernl does not serve (config, cvm, upload, fs, llm, cache) have a row in DOMAIN_CAPABILITIES so a grant can be expressed, but the shell does not offer them and grants.allowDomain refuses them with invalid:domain. Identity and grants explains how the set is narrowed.

The mount path ​

createAppletLifecycle (kernl-lifecycle.ts) turns an address into a running window in four steps, and the order is load-bearing:

  1. decodePointer(input) (applets/pointer.ts) turns the address into { kind: 'address' | 'event', … } and nothing more. Trust comes from verifying the event, never from the pointer.
  2. resolve(pointer) fetches the manifest from the pointer's relay hints plus the apps tier (fetchManifestEvent), then verifies the signature and the artifact bytes by hash through resolveApplet over kehto's resolveNapplet (applets/resolve.ts). Cached bytes are re-hashed on every read. Gateways and caches are accelerators, never identity.
  3. plan(summary) calls grants.ensure(identity) and installs the applet (The store and intents), then planMount (applets/plan.ts): it refuses markup that declares a frame (denied:nested-frame), then builds the environment from resolveShellEnvironment, the guard's CSP and the namespace prelude inserted by prepareNappletSrcdoc, the Trusted Types directives and frame lock spliced in after them (D-014), sandbox: 'allow-scripts', and missingRequired.
  4. mount(plan, frame) installs the profile's rates in kehto's runtime firewall with applyRuntimeFirewallRules, then mountApplet (applets/mount.ts) hardens the frame, registers the window in originRegistry, sets the environment, calls bridge.observeFrame, and only then assigns srcdoc. The frame has sandbox="allow-scripts" and denyNappletControllerAccess applied; it never has src and never allow-same-origin.

Every revocation reason (window-message, second-hello, invalid-hello, second-load, port-closed, host) and a guard quarantine removes the iframe and drops the guard's state for that window. Security posture lists the rules this path must keep.

The app ​

apps/web is Astro 7 with Svelte 5 islands. It owns the landing at /, the long-form pages under /about/, the shell at /app/, and the KernlHost that gives @kernlize/core a page: localStorage, the consent dialog, the handler chooser, notices, link confirmation, opening and focusing windows, and window visibility. It creates the iframes, wires window.addEventListener('message', kernl.handleWindowMessage) once, and renders the store, the grants panel per window, the guard panel and the provider picker. Where windows go, which is focused and what the guard is told about each belong to a WindowManager the app makes in one file, windows.ts, so that they can be changed or replaced there (Layouts, D-022). The published packages contain none of this; the app is the only framework-dependent workspace (D-004). ?preview=1 swaps in preview-kernl.ts, which implements the same Kernl contract without a network (D-011). The site's pages cross into one another without reloading, behind a WebGL curtain; /app/ takes no part in that and is always a fresh page load, so its frame policy is in the HTML the server sent (D-023).

MIT licensed. Protocol authority: NIP-5D (nostr-protocol/nips PR #2303) and the NAPs (napplet/naps).