From key protection to login flow. The complete picture.
NSE protects keys. But keys need to be used — for signing events, proving identity,
authorizing actions. This guide shows how NSE fits into the three ways Nostr signing
actually works in practice.
What is a Bunker?
If you've never heard the term "bunker" in Nostr context, here's the short version:
a bunker is a signer that holds your private key and signs on your behalf.
The app that needs a signature never sees the key. It sends a signing request, gets back a signature.
The communication happens over Nostr relays using kind 24133 ephemeral events,
encrypted end-to-end. The relay is just a message bus — it can't read the requests or responses.
Think of it like SSH agent forwarding. Your SSH key stays on one machine.
When another machine needs to authenticate, the agent signs on its behalf. The key never moves.
Same idea, different protocol.
NIP-46 defines this protocol. The methods are straightforward:
sign_event, get_public_key, connect,
nip04_encrypt, nip04_decrypt, and a few more.
The signer implements them. The client calls them. The relay carries the messages.
Where NSE Fits
This is the important distinction: NSE is not the bunker.
NSE is what the bunker uses internally to protect the key at rest.
Without NSE: Bunker stores nsec in plaintext
→ device compromised → key gone
With NSE: Bunker stores encrypted blob
→ device compromised → attacker gets ciphertext, not key
NSE is the lock on the vault door. The bunker is the vault.
The relay is the hallway between rooms. Three different things, each with its own job.
Your application — whether it's a browser extension, a phone app, or a server process —
is the bunker. It uses NSE to decrypt the key only when it needs to sign, then zeros the plaintext
immediately after. The rest of the time, the key is AES-256-GCM ciphertext protected by hardware.
Three Patterns
There are three ways signing works in practice. Which one you use depends on where the
signer lives relative to the app that needs signatures.
Pattern A
Local Signing (NIP-07)
When: Browser extension, same device, no network needed.
The simplest pattern. Your browser extension IS the signer. A web app calls
window.nostr.signEvent(event) — that's the NIP-07 standard.
The extension receives the request, uses NSE to decrypt the key, signs the event
with Schnorr, zeros the plaintext, and returns the signature. Done.
The Nostr network is not involved in the signing. Relays carry the signed event
after — but the act of signing is entirely local. No relay, no network, no discovery.
Just local IPC.
Web AppBrowser Extension
│ │
├── window.nostr.signEvent() ──→│
│ ├── NSEBrowser.sign(event)
│ │ ├── Decrypt secp256k1 key
│ │ ├── Schnorr sign
│ │ └── Zero plaintext
│←── signed event ─────────────⊣
│ │
├── Publish to relay ────────→ Nostr Network
Extension side (the signer)
// In your browser extension's background script import { NSEBrowser, NSEIndexedDBStorage } from'nostr-secure-enclave-browser';
// In your web app — the requesting side const signed = await window.nostr.signEvent({
kind: 1,
content: 'Hello Nostr!',
tags: [],
created_at: Math.floor(Date.now() / 1000),
});
// signed.id, signed.sig, signed.pubkey are populated
Key point: No relay. No network. No discovery. Just local IPC.
This is how NostrKey works.
Pattern B
Remote Signing — Your Relay
When: Phone signs for desktop. You control both ends and the relay.
The phone has the key (protected by NSE + Secure Enclave or StrongBox).
The desktop app needs a signature. Both connect to your relay —
say relay.nostrkeep.com. The desktop sends a NIP-46 signing request through the relay.
The phone receives it, decrypts the key with NSE, signs, and sends the signature back through the relay.
No bunker:// URI parsing. No public relay discovery. No hoping some random relay is online.
You built both apps, you know where to connect.
// NIP-46 signer — the side that holds the key import { NSEServer } from'nostr-secure-enclave-server'; // On mobile: import NSE from 'nostr-secure-enclave-ios' // or: import NSE from 'nostr-secure-enclave-android'
const signed = awaitrequestSignature({
kind: 1,
content: 'Signed from desktop, key on phone!',
tags: [],
created_at: Math.floor(Date.now() / 1000),
});
Key point: You own the relay. Both sides know where to connect.
No discovery protocol needed. This is how
NostrKeep Signer works with the browser extension.
Any NIP-01 relay that supports ephemeral events works. The client parses the URI,
connects to the specified relay, and follows the NIP-46 handshake. NSE still protects
the key on the signer side — the relay just carries encrypted messages.
Key point: This is the fully decentralized version. Any app can request
signatures from any NSE-powered signer through any relay. Maximum interoperability,
but requires relay discovery via bunker:// URIs.
Choosing Your Pattern
Question
Pattern
Signer and app on the same device?
A — Local (NIP-07)
You control both apps + the relay?
B — Your Relay
Third-party apps or general interop?
C — Any Relay (NIP-46)
Most products start with Pattern A (browser extension) and add Pattern B
(mobile bunker) later. Pattern C is for when you want your signer to work with the broader
Nostr ecosystem — any app, any relay.
The good news: NSE works identically in all three patterns. The nse.sign(event) call
doesn't know or care how the signing request arrived. It just decrypts, signs, and zeros.
The Relay Question
"Do I need to run a relay?" is the most common question. The answer depends on your pattern:
Pattern A: No. There is no relay. Signing is local.
Pattern B: Yes, but it's just a WebSocket message bus. Any NIP-01 relay works. NostrKeep's relay (relay.nostrkeep.com) is deployed and free for ephemeral NIP-46 traffic. Or run your own — a minimal NIP-01 relay is a surprisingly small amount of code.
Pattern C: The signer specifies which relay to use in the bunker:// URI. Your users connect to that relay. You can use an existing public relay or host your own.
The relay does not see keys. It only sees encrypted NIP-46 messages
(kind 24133, ephemeral). Even a compromised relay can't extract signatures or keys —
it's just passing encrypted envelopes. The encryption is end-to-end between the client
and the signer, using NIP-04 or NIP-44.
What You're Building On
Here's how all the pieces fit together, from your application down to hardware:
Direct (library) means calling nse.sign() directly in your code —
no NIP-07, no NIP-46. This is what bots, AI entities, and server processes use.
Same NSE underneath, just no signing protocol layer on top.
The nostr-crypto-utils library includes
NIP-46 client and server-side signer primitives if you need a head start on the
encryptNip46Request / decryptNip46Response functions shown above.
Next Steps
Pick a pattern and start building.
Start with Pattern A —
install nostr-secure-enclave-browser
and protect keys in your extension.
Add Pattern B —
install nostr-secure-enclave-ios or
nostr-secure-enclave-android,
connect through your relay.
Go full NIP-46 —
your signer works with any Nostr app through any relay. Maximum interoperability.