Getting started
This page walks you from zero to a deployable napplet. The standalone napplet CLI provisions its own cached tooling and does not require host Node.js, npm, npx, or Deno, including for screenshots and conformance. You will need Node.js 20+ and a package manager to develop the generated Vite project or use the optional skills.sh CLI. To actually run a napplet you also need a host shell — see running in a shell at the end.
1. Install the CLI
# macOS or Linux
curl -fsSL https://napplet.run/install.sh | sh# Windows PowerShell
irm https://napplet.run/install.ps1 | iexEach installer downloads the platform binary and verifies it against the release's SHA256SUMS before replacing napplet. The @napplet/cli page documents the JSR/Deno alternative.
Run napplet guide at any point for the complete command sequence and links to the documentation for each stage.
2. Create the project
napplet create my-napplet
cd my-nappletnapplet create delegates to the maintained github.com/napplet/boilerplate template. The generator creates the project and derives only its package name; it does not own deployment name, title, or archetype metadata.
3. Initialize deployment metadata
napplet initThe wizard writes .napplet/config.yaml with the named-manifest d-tag, title, optional description, queryless napplet:<archetype>/<intent> convention metadata, relays, and Blossom servers. Automation can provide the same values explicitly:
napplet init --name my-napplet --title "My Napplet" \
--archetype napplet:note/open \
--relay wss://relay.example --server https://blossom.exampleThe CLI validates the documented napplet:<archetype>/<intent> shape and deploys the complete queryless convention identity.
At runtime, INC emit and intent invoke/open may accept napplet:<archetype>/<intent>?name=value developer input. Their bindings transpose query pairs to text payload fields before sending a queryless normalized identity. Subscriptions, manifest discovery, and handler routing do not parse queries and use exact equality.
4. Install agent skills
npx skills add napplet/webThe open skills CLI detects the coding agents on your machine (Claude Code, Codex, Cursor, Gemini CLI, Copilot, and many more) and installs the napplet-* skills where each agent reads them. Add -g for a global install, --skill napplet-make for a single skill, or -a claude-code to target one agent. See Agent skills for what each skill does.
5. Build and verify
pnpm install
# Ask your agent to build the napplet.
pnpm verify
pnpm test:conformance6. Preview, then deploy
napplet deploy --dry-run
napplet deployThe dry run resolves the same config, files, metadata, and manifest events without uploading or publishing. Configure a local or remote signer before the live deploy; see @napplet/cli.
Choose the Note Drafts path that matches how you want to learn:
- Build a Note Drafts napplet from scratch hand-wires every file so you can see the runtime boundary one piece at a time.
- Build a Note Drafts napplet from boilerplate starts from the generator, preserves its project substrate, and replaces the starter demo with the same note composer.
- Build a Note Drafts napplet with an AI agent and the napplet skills shows the prompt, review loop, and verification commands for agent-assisted authoring.
All three create a small Nostr note composer that autosaves through storage, reads the shell-user pubkey through identity, publishes through outbox, and runs in Kehto/Paja.
Retrofit an existing app manually
For a new napplet, prefer the generator above. It owns the package manager pin, Vite config, single-file build plumbing, scripts, conformance wiring, and starter layout. Use manual wiring only when you are adding napplet support to an existing app.
Install the napplet-side SDK:
npm install @napplet/sdkAnd the build-time Vite plugin as a dev dependency, which generates the signed NIP-5A manifest event:
npm install -D @napplet/vite-plugin// vite.config.ts
import { defineConfig } from 'vite';
import { nip5aManifest } from '@napplet/vite-plugin';
export default defineConfig({
plugins: [nip5aManifest({ nappletType: 'my-napplet', artifactMode: 'single-file' })],
});Mirror the boilerplate scripts so verification stays consistent:
{
"scripts": {
"build": "vite build",
"type-check": "tsc --noEmit",
"verify": "npm run type-check && npm run build",
"test:conformance": "npm run build && napplet-conformance ./dist"
}
}Build your first napplet
NIP-5D runtimes inject window.napplet before your code runs. Napplet code can use the injected namespace directly:
// main.ts
// Read kind 1 notes through the shell's outbox-aware routing
const { events } = await window.napplet.outbox.query(
[{ kinds: [1], limit: 20 }],
{ timeoutMs: 3000 },
);
for (const result of events) console.log('Note:', result.event.content);
// Subscribe to live updates through the same outbox boundary const sub = window.napplet.outbox.subscribe([{ kinds: [1], limit: 20 }], { timeoutMs: 3000, }); sub.on('event', (result) => console.log('New note:', result.event.content));
// Publish a note — the shell signs and fans it out; your napplet never sees the key
const published = await window.napplet.outbox.publish({
kind: 1,
content: 'Hello from my napplet!',
tags: [],
created_at: Math.floor(Date.now() / 1000),
});
if (!published.ok) throw new Error(published.error ?? 'publish failed');Notice you never imported a signing library and never touched a relay socket. The runtime sends outbox.query, outbox.subscribe, and outbox.publish JSON envelopes; the shell does relay discovery, signing, fanout, and policy.
Runtime injection vs. SDK
Runtimes and napplets use different packages.
@napplet/shim | @napplet/sdk | |
|---|---|---|
| Import style | import { installNappletGlobal } from '@napplet/shim' | import { outbox, inc, intent } from '@napplet/sdk' |
| Who uses it | Host runtime before napplet scripts execute | Napplet application code |
| What it does | Injects selected window.napplet.<domain> objects | Named, typed exports that wrap injected domains |
| Required? | Required for runtimes that use these packages | Optional convenience for bundler users |
The sdk is an ergonomic, typed wrapper. Its methods delegate to window.napplet.* at call time and throw a clear error if the runtime did not inject the namespace or requested domain.
Typical napplet code imports only the SDK:
import { outbox, inc, intent, storage } from '@napplet/sdk';If you're writing a vanilla napplet with no build step, you can skip the sdk and use the injected window.napplet.* namespace directly.
For cross-napplet intent, call intent.invoke(uri, options?) or intent.open(uri, options?) with a convention URI such as napplet:profile/open?pubkey=abc123. An ok: true result means the runtime accepted delivery responsibility; it does not mean the target has already handled anything. An ok: false result carries error. The target receives the delivery through intent.onDelivery(handler) as a separate push with a runtime-attested sender, independent of the source lifetime and without any NAP-INC dependency. This is non-normative orientation; the living NAP-INTENT, NAP-INC, and web projection documents are authoritative.
Running in a shell
A napplet only does something when hosted by a compatible shell — the trusted runtime that owns keys, relays, and storage and proxies your requests. The reference runtime is Kehto.
TIP
The reference shell/runtime is Kehto: github.com/kehto/web. You can also find napplet projects under the github.com/napplet organization. For questions and coordination, join the community group chat.
Next, read Core concepts to understand the envelope, NAPs, the sandbox model, identity, and storage scoping.
Terminal and automation options
napplet init and napplet deploy guide you through missing settings in a terminal. Deployment previews and results use readable sections, and network deployments show upload and relay progress. For automation, add -i / --non-interactive and supply values through flags or configuration. CI and redirected streams never trigger prompts. Use napplet deploy -i --json for machine-readable results and --no-color for plain display. See the CLI reference for all options.