nostr: URI scheme
NIP-21 standardizes nostr: URIs (like nostr:npub1...) enabling universal links to profiles, notes, events that work across all Nostr clients.
NIP-21: nostr: URI scheme
Status: Draft Author: fiatjaf Category: Application
Overview
NIP-21 defines the nostr: URI scheme for creating universal links to Nostr entities (profiles, notes, events) that work across all clients and platforms.
Core Concept:
- Standard URI format:
nostr:<bech32-encoded-entity> - Works like
mailto:,tel:,bitcoin: - Operating system routes to user’s preferred Nostr client
- Cross-platform, cross-client compatibility
Supported Entities:
- ✅
nostr:npub1...- User profiles - ✅
nostr:note1...- Individual notes - ✅
nostr:nprofile1...- Profiles with relay hints - ✅
nostr:nevent1...- Events with relay hints - ✅
nostr:naddr1...- Parameterized replaceable events
Status: Draft (widely adopted)
Why nostr: URIs Matter
Universal Sharing
Without nostr: URIs:
- Share raw hex IDs (not user-friendly)
- Client-specific URLs (don’t work everywhere)
- No standard sharing format
- Poor mobile/OS integration
With nostr: URIs:
- Universal format works in any client
- OS integration (click to open in favorite app)
- QR codes for easy sharing
- Cross-platform (web, mobile, desktop)
- Future-proof standard
Use Cases
- Social Sharing: Share profiles/posts universally
- QR Codes: Encode Nostr links in QR codes
- Deep Linking: Open specific content in apps
- Cross-Client: Link between different Nostr clients
- Embeds: Reference Nostr content in web pages
- NFC Tags: Nostr links in physical objects
URI Format
Basic Structure
nostr:<bech32-entity>
Examples:
nostr:npub1a2b3c4d... (public key)
nostr:note1x9y8z7w... (note ID)
nostr:nprofile1q2r3s4t... (profile with relay)
nostr:nevent1u6v5w4x... (event with relay)
nostr:naddr1y8z7a6b... (addressable event)
Entity Types
1. npub (Public Key)
Link to a user profile:
nostr:npub1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z
Use: Link to any user’s profile
import { nip19 } from 'nostr-tools';
function createProfileURI(pubkeyHex) {
const npub = nip19.npubEncode(pubkeyHex);
return `nostr:${npub}`;
}
// Usage
const uri = createProfileURI("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d");
// Result: nostr:npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6
console.log(uri);
// Click to view this profile in your Nostr client
2. note (Note ID)
Link to a specific note/event:
nostr:note1abc123def456...
Use: Share individual posts
function createNoteURI(eventIdHex) {
const note = nip19.noteEncode(eventIdHex);
return `nostr:${note}`;
}
// Usage
const uri = createNoteURI("d94a32a7e384f8d8e72a56d5d9d8c73e8f4a56b7c8d9e0f1a2b3c4d5e6f7g8h9");
// Result: nostr:note1m99r9flrsnudumwp2m2an0vw86...
3. nprofile (Profile with Relay Hints)
Profile link with recommended relays:
nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p
Use: Ensure profile can be fetched from specific relays
function createProfileWithRelaysURI(pubkey, relays) {
const nprofile = nip19.nprofileEncode({
pubkey: pubkey,
relays: relays
});
return `nostr:${nprofile}`;
}
// Usage
const uri = createProfileWithRelaysURI(
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
["wss://relay.damus.io", "wss://relay.primal.net"]
);
// Clients will try these relays first to fetch the profile
4. nevent (Event with Relay Hints)
Event link with relay hints and optional author:
nostr:nevent1qqsth8v...
Use: Share specific events with context
function createEventURI(eventId, author, relays) {
const nevent = nip19.neventEncode({
id: eventId,
author: author, // Optional
relays: relays, // Optional but recommended
kind: 1 // Optional
});
return `nostr:${nevent}`;
}
// Usage
const uri = createEventURI(
"d94a32a7e384f8d8e72a56d5d9d8c73e8f4a56b7c8d9e0f1a2b3c4d5e6f7g8h9",
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
["wss://relay.damus.io"]
);
5. naddr (Parameterized Replaceable Event)
Link to addressable events (NIP-33):
nostr:naddr1qqxnzd3e...
Use: Link to articles, profiles, or other replaceable content
function createAddressableEventURI(kind, pubkey, identifier, relays) {
const naddr = nip19.naddrEncode({
kind: kind,
pubkey: pubkey,
identifier: identifier, // d tag value
relays: relays
});
return `nostr:${naddr}`;
}
// Usage: Link to long-form article (NIP-23)
const uri = createAddressableEventURI(
30023, // Article kind
"author-pubkey-hex",
"my-article-slug",
["wss://relay.damus.io"]
);
Creating URIs
Complete URI Generator
import { nip19 } from 'nostr-tools';
class NostrURI {
static profile(pubkey, relays = []) {
if (relays.length > 0) {
return `nostr:${nip19.nprofileEncode({ pubkey, relays })}`;
}
return `nostr:${nip19.npubEncode(pubkey)}`;
}
static note(eventId, relays = [], author = null) {
if (relays.length > 0 || author) {
const data = { id: eventId };
if (relays.length > 0) data.relays = relays;
if (author) data.author = author;
return `nostr:${nip19.neventEncode(data)}`;
}
return `nostr:${nip19.noteEncode(eventId)}`;
}
static addressable(kind, pubkey, identifier, relays = []) {
return `nostr:${nip19.naddrEncode({
kind,
pubkey,
identifier,
relays
})}`;
}
}
// Usage
const profileURI = NostrURI.profile(myPubkey, ["wss://relay.damus.io"]);
const noteURI = NostrURI.note(eventId, ["wss://nos.lol"], authorPubkey);
const articleURI = NostrURI.addressable(30023, authorPubkey, "article-slug", relays);
Parsing URIs
Extract Entity from URI
function parseNostrURI(uri) {
// Remove nostr: prefix
const bech32 = uri.replace(/^nostr:/, '');
// Determine type from prefix
const prefix = bech32.substring(0, bech32.indexOf('1'));
switch (prefix) {
case 'npub':
return {
type: 'profile',
pubkey: nip19.decode(bech32).data
};
case 'note':
return {
type: 'note',
eventId: nip19.decode(bech32).data
};
case 'nprofile':
const profile = nip19.decode(bech32).data;
return {
type: 'profile',
pubkey: profile.pubkey,
relays: profile.relays || []
};
case 'nevent':
const event = nip19.decode(bech32).data;
return {
type: 'event',
eventId: event.id,
author: event.author,
relays: event.relays || [],
kind: event.kind
};
case 'naddr':
const addr = nip19.decode(bech32).data;
return {
type: 'addressable',
kind: addr.kind,
pubkey: addr.pubkey,
identifier: addr.identifier,
relays: addr.relays || []
};
default:
throw new Error(`Unknown nostr URI type: ${prefix}`);
}
}
// Usage
const parsed = parseNostrURI("nostr:npub180cvv07...");
console.log(parsed.type); // "profile"
console.log(parsed.pubkey); // "3bf0c63..."
Client Handling
Register URI Handler (Desktop)
// Electron app
const { app, protocol } = require('electron');
app.on('ready', () => {
// Register nostr: protocol
protocol.registerHttpProtocol('nostr', (request, callback) => {
const uri = request.url;
handleNostrURI(uri);
});
// Set as default handler
app.setAsDefaultProtocolClient('nostr');
});
function handleNostrURI(uri) {
const parsed = parseNostrURI(uri);
switch (parsed.type) {
case 'profile':
// Open profile view
openProfile(parsed.pubkey);
break;
case 'note':
case 'event':
// Open note/event view
openEvent(parsed.eventId, parsed.relays);
break;
case 'addressable':
// Open addressable event
openAddressableEvent(parsed);
break;
}
}
Deep Linking (Mobile)
// React Native / iOS
import { Linking } from 'react-native';
// Listen for nostr: URIs
Linking.addEventListener('url', ({ url }) => {
if (url.startsWith('nostr:')) {
handleNostrURI(url);
}
});
// Register handler in info.plist
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>nostr</string>
</array>
</dict>
</array>
// Android: Add intent filter in AndroidManifest.xml
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="nostr" />
</intent-filter>
Web App Handling
// Detect nostr: links clicked on page
document.addEventListener('click', (e) => {
const link = e.target.closest('a');
if (!link) return;
const href = link.getAttribute('href');
if (href && href.startsWith('nostr:')) {
e.preventDefault();
// Handle in web app
handleNostrURI(href);
// Or let OS handle it
// window.location.href = href;
}
});
function handleNostrURI(uri) {
const parsed = parseNostrURI(uri);
switch (parsed.type) {
case 'profile':
// Navigate to profile page
window.location.href = `/profile/${parsed.pubkey}`;
break;
case 'note':
// Navigate to note page
window.location.href = `/note/${parsed.eventId}`;
break;
}
}
Sharing Features
Share Button Component
function ShareButton({ event }) {
const uri = NostrURI.note(event.id, userRelays, event.pubkey);
async function handleShare() {
if (navigator.share) {
// Native share on mobile
await navigator.share({
title: "Check out this Nostr post",
text: event.content.substring(0, 100),
url: uri
});
} else {
// Copy to clipboard
await navigator.clipboard.writeText(uri);
alert("Link copied!");
}
}
return (
<button onClick={handleShare}>
Share Post
</button>
);
}
QR Code Generation
import QRCode from 'qrcode';
async function generateNostrQR(uri) {
const qrDataURL = await QRCode.toDataURL(uri, {
width: 300,
margin: 2,
color: {
dark: '#000000',
light: '#ffffff'
}
});
return qrDataURL;
}
// Usage Component
function ProfileQRCode({ pubkey }) {
const [qrCode, setQRCode] = useState(null);
useEffect(() => {
const uri = NostrURI.profile(pubkey);
generateNostrQR(uri).then(setQRCode);
}, [pubkey]);
return (
<div>
<h3>Scan to follow:</h3>
{qrCode && <img src={qrCode} alt="Nostr Profile QR" />}
</div>
);
}
URL vs URI
When to Use Each
nostr: URI (protocol handler):
- Native app integration
- OS-level routing
- Mobile deep links
- QR codes
HTTP URLs (web links):
- SEO-friendly
- Social media previews
- Web-first experiences
- Fallback for non-Nostr users
Best Practice: Provide both
function createShareLinks(event) {
return {
// Universal nostr: URI
uri: NostrURI.note(event.id, relays, event.pubkey),
// Web URL (client-specific)
web: `https://snort.social/e/${nip19.noteEncode(event.id)}`,
// Alternative: universal web link
universal: `https://nostr.com/${nip19.noteEncode(event.id)}`
};
}
Security Considerations
URI Validation
function validateNostrURI(uri) {
// Check format
if (!uri.startsWith('nostr:')) {
return { valid: false, error: "Not a nostr: URI" };
}
try {
// Try to parse
const parsed = parseNostrURI(uri);
// Verify hex lengths
if (parsed.pubkey && parsed.pubkey.length !== 64) {
return { valid: false, error: "Invalid pubkey length" };
}
if (parsed.eventId && parsed.eventId.length !== 64) {
return { valid: false, error: "Invalid event ID length" };
}
return { valid: true, parsed };
} catch (error) {
return { valid: false, error: error.message };
}
}
// Usage: Validate before processing
const result = validateNostrURI(userInput);
if (!result.valid) {
console.error("Invalid URI:", result.error);
} else {
handleNostrURI(userInput);
}
Phishing Protection
function warnBeforeOpeningURI(uri) {
const parsed = parseNostrURI(uri);
// Warn on sensitive actions
if (parsed.type === 'addressable' && parsed.kind === 3) {
return confirm(
"This link will update your contact list. Are you sure?"
);
}
return true;
}
Client Support
Clients with nostr: URI Support
- Damus - Full support, OS integration
- Amethyst - Deep linking, QR codes
- Primal - URI handling in-app
- Snort - Web-based URI parsing
- Iris - Click-to-open URIs
- Nostrudel - Comprehensive URI support
Most modern Nostr clients support NIP-21.
Common Questions
What’s the difference between npub and nprofile?
npub- Just the public keynprofile- Public key + recommended relays (better for sharing)
Can I use nostr: links on Twitter/Facebook?
Yes, but they won’t have previews. Use web URLs (njump.me, snort.social) for social media sharing with previews.
Do nostr: links work in browsers?
Only if a Nostr client is installed and registered as the protocol handler. Otherwise, use web URLs.
How do I create a nostr: link for my profile?
Use nostr: + your npub (from NIP-19 encoding of your pubkey).
Are nostr: URIs case-sensitive?
The bech32 encoding is case-insensitive, but use lowercase by convention.
Related NIPs
- NIP-01 - Basic protocol (event structure)
- NIP-19 - bech32-encoded entities (npub, note, etc.)
- NIP-27 - Text note references (inline nostr: links)
Technical Specification
For the complete technical specification, see NIP-21 on GitHub.
Summary
NIP-21 standardizes nostr: URIs for universal linking:
✅ nostr: protocol for OS-level integration ✅ Multiple entity types: npub, note, nprofile, nevent, naddr ✅ Relay hints for better discoverability ✅ Cross-platform compatibility ✅ Deep linking in mobile apps
URI examples:
nostr:npub1... (profile)
nostr:note1... (note)
nostr:nprofile1... (profile with relays)
nostr:nevent1... (event with context)
nostr:naddr1... (addressable event)
Status: Draft - widely adopted across clients.
Best practice: Include relay hints for better content discovery.
Next Steps:
- Learn about text references in NIP-27
- Explore bech32 encoding in NIP-19
- Understand parameterized events in NIP-33
- Browse all NIPs in our reference
Last updated: October 2023 Official specification: GitHub
Client Support
This NIP is supported by the following clients: