NIP-21

nostr: URI scheme

draft application

NIP-21 standardizes nostr: URIs (like nostr:npub1...) enabling universal links to profiles, notes, events that work across all Nostr clients.

Author
fiatjaf
Last Updated
10 October 2023
Official Spec
View on GitHub →

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

  1. Social Sharing: Share profiles/posts universally
  2. QR Codes: Encode Nostr links in QR codes
  3. Deep Linking: Open specific content in apps
  4. Cross-Client: Link between different Nostr clients
  5. Embeds: Reference Nostr content in web pages
  6. 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 key
  • nprofile - Public key + recommended relays (better for sharing)

Yes, but they won’t have previews. Use web URLs (njump.me, snort.social) for social media sharing with previews.

Only if a Nostr client is installed and registered as the protocol handler. Otherwise, use web URLs.

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.


  • 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:


Last updated: October 2023 Official specification: GitHub

Client Support

This NIP is supported by the following clients:

damus amethyst primal snort iris
View all clients →

Related NIPs

NIP-01 NIP-19 NIP-27
← Browse All NIPs