This primer introduces Nostr developers to DIDs (Decentralized Identifiers) and shows how to bridge Nostr's social protocol with the broader web identity ecosystem. You'll go from zero to production-ready DID integration in incremental steps, with immediate wins at each stage.

This primer is designed for developers already familiar with Nostr who want to understand and implement DID-Nostr integration. Each section builds on the previous, with working code examples you can test immediately.

🚀 Quick Win: Your First DID (2 minutes)

Let's start with immediate gratification. If you have a Nostr pubkey, you already have a DID:

// Your Nostr pubkey (64-character hex)
const pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245";

// Your DID is just:
const did = `did:nostr:${pubkey}`;
// → "did:nostr:32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"

console.log("🎉 You have a DID:", did);
      

That's it! You now have a globally unique, web-compatible identifier. No registration, no servers, no fees. Just your existing Nostr key in a new format.

What You Just Gained

🏗️ Stage 1: From Events to Documents

You know Nostr events. DIDs use "DID Documents" - think of them as persistent, structured profiles that web services can discover and trust.

Nostr Event vs DID Document

What You Know: Nostr Kind 0 Event

{
  "kind": 0,
  "created_at": 1737906600,
  "content": "{\"name\":\"Alice\",\"about\":\"Building nostr\",\"picture\":\"https://...\"}",
  "pubkey": "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
  "sig": "..."
}
        

What You Get: DID Document

{
  "@context": ["https://w3id.org/did", "https://w3id.org/nostr/context"],
  "id": "did:nostr:32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
  "verificationMethod": [{
    "id": "#key1",
    "type": "Multikey",
    "controller": "did:nostr:32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
    "publicKeyMultibase": "fe70102..."
  }],
  "authentication": ["#key1"],
  "profile": {
    "name": "Alice", 
    "about": "Building nostr",
    "picture": "https://...",
    "timestamp": 1737906600
  }
}
        

Key Differences (Why This Matters)

AspectNostr EventDID Document
Audience Nostr clients only Any web service
Discovery Query specific relays HTTP, DNS, standardized
Verification Schnorr signature Cryptographic proof + web standards
Caching Client-specific HTTP caching, CDNs work
Integration Custom Nostr code Standard DID libraries

⚡ Stage 2: Resolution - Make It Work

Now let's make your DID actually do something. DID Resolution is how you turn a DID string into useful data. Think of it like DNS - turn a name into something actionable.

Three Ways to Resolve (Pick Your Style)

Option 1: Instant Offline (Zero Network)

Perfect for mobile apps, offline scenarios, or when you just need the basics fast.

function resolveDidOffline(did) {
  // Extract pubkey from DID
  const pubkey = did.replace('did:nostr:', '');
  
  // Generate minimal document instantly
  return {
    "@context": ["https://w3id.org/did", "https://w3id.org/nostr/context"],
    "id": did,
    "verificationMethod": [{
      "id": `${did}#key1`,
      "type": "Multikey", 
      "controller": did,
      "publicKeyMultibase": pubkeyToMultikey(pubkey)
    }],
    "authentication": [`${did}#key1`]
  };
}

// Works immediately, no network needed!
const didDoc = resolveDidOffline("did:nostr:32e182...");
console.log("✨ Instant DID document:", didDoc);
        

Option 2: HTTP Resolution (Web Native)

Best for web apps - uses standard HTTP caching, CDNs, all the web performance tricks you know.

async function resolveDidHttp(did) {
  const pubkey = did.replace('did:nostr:', '');
  const url = `https://example.com/.well-known/did/nostr/${pubkey}.json`;
  
  try {
    const response = await fetch(url);
    if (response.ok) {
      // Check if data is fresh
      const timestamp = response.headers.get('Nostr-Timestamp');
      const age = Date.now()/1000 - parseInt(timestamp);
      
      if (age < 3600) { // Fresh within 1 hour
        return response.json();
      }
    }
  } catch (e) {
    console.log("HTTP failed, falling back...");
  }
  
  // Fallback to offline resolution
  return resolveDidOffline(did);
}

// Web-optimized with automatic fallback
const didDoc = await resolveDidHttp("did:nostr:32e182...");
        

Option 3: Enhanced with Relays (Full Social)

When you want the complete social profile with all the metadata.

async function resolveDidEnhanced(did) {
  // Start with offline base
  let didDoc = resolveDidOffline(did);
  
  try {
    // Query your favorite relays
    const relays = ["wss://relay.damus.io", "wss://nos.lol"];
    const pubkey = did.replace('did:nostr:', '');
    
    // Get kind 0 profile
    const profile = await fetchProfileFromRelays(pubkey, relays);
    if (profile) {
      didDoc.profile = {
        name: profile.name,
        about: profile.about, 
        picture: profile.picture,
        nip05: profile.nip05,
        timestamp: profile.created_at
      };
    }
    
    // Get relay list (kind 10002)
    const userRelays = await fetchRelaysFromRelays(pubkey, relays);
    if (userRelays.length > 0) {
      didDoc.service = userRelays.map((relay, i) => ({
        id: `#relay${i+1}`,
        type: "Relay",
        serviceEndpoint: relay
      }));
    }
  } catch (e) {
    console.log("Enhanced resolution failed, using basic document");
  }
  
  return didDoc;
}

// Full-featured with graceful degradation
const didDoc = await resolveDidEnhanced("did:nostr:32e182...");
        

🎯 Pro Tip: Layered Strategy

Combine all three for maximum performance and reliability:

async function smartResolve(did) {
  // 1. Try HTTP first (fastest if cached)
  try {
    const httpDoc = await resolveDidHttp(did);
    if (httpDoc.profile?.timestamp) {
      const age = Date.now()/1000 - httpDoc.profile.timestamp;
      if (age < 1800) return httpDoc; // Fresh within 30 min
    }
  } catch (e) {}
  
  // 2. Try enhanced resolution (full features)
  try {
    return await resolveDidEnhanced(did);
  } catch (e) {}
  
  // 3. Fallback to offline (always works)
  return resolveDidOffline(did);
}

// This always succeeds, with best possible data
const didDoc = await smartResolve("did:nostr:32e182...");
        

🔧 Stage 3: Integration Patterns

Now let's integrate DIDs into real applications. Here are the most common patterns that work great with existing Nostr apps.

Pattern 1: Profile Cache Enhancement

Upgrade your existing profile caching with DID superpowers:

class ProfileManager {
  async getProfile(pubkey) {
    // Check local cache first
    let profile = this.cache.get(pubkey);
    
    if (!profile || this.isStale(profile)) {
      // Try DID resolution for fresh data
      const did = `did:nostr:${pubkey}`;
      const didDoc = await smartResolve(did);
      
      if (didDoc.profile) {
        profile = {
          ...didDoc.profile,
          source: 'did',
          cachedAt: Date.now()
        };
        this.cache.set(pubkey, profile);
      } else {
        // Fallback to relay query
        profile = await this.fetchFromRelays(pubkey);
      }
    }
    
    return profile;
  }
  
  isStale(profile) {
    const age = (Date.now() - profile.cachedAt) / 1000;
    return age > 3600; // 1 hour
  }
}

// Drop-in upgrade for existing apps
const profileManager = new ProfileManager();
const profile = await profileManager.getProfile("32e182...");
        

Pattern 2: Multi-Protocol Identity

Bridge Nostr with other systems seamlessly:

class IdentityBridge {
  async linkAccounts(nostrPubkey) {
    const did = `did:nostr:${nostrPubkey}`;
    const didDoc = await smartResolve(did);
    
    const identity = {
      did,
      nostr: {
        pubkey: nostrPubkey,
        npub: nip19.npubEncode(nostrPubkey)
      },
      profile: didDoc.profile,
      verificationMethods: didDoc.verificationMethod,
      // Add other protocols
      mastodon: didDoc.profile?.nip05 ? await this.resolveMastodon(didDoc.profile.nip05) : null,
      github: didDoc.service?.find(s => s.type === 'GitHub')?.serviceEndpoint,
      website: didDoc.service?.find(s => s.type === 'Website')?.serviceEndpoint
    };
    
    return identity;
  }
}

// Universal identity object
const identity = await bridge.linkAccounts("32e182...");
console.log("Universal identity:", identity);
        

Pattern 3: Authentication Enhancement

Add DID auth to your existing Nostr login:

class NostrDIDAuth {
  async authenticate(pubkey, signature) {
    // Standard Nostr auth
    const validNostr = await this.verifyNostrSignature(pubkey, signature);
    if (!validNostr) throw new Error("Invalid Nostr signature");
    
    // Enhance with DID context
    const did = `did:nostr:${pubkey}`;
    const didDoc = await smartResolve(did);
    
    // Return enhanced auth context
    return {
      authenticated: true,
      pubkey,
      did,
      profile: didDoc.profile,
      capabilities: this.extractCapabilities(didDoc),
      // Standard JWT-compatible claims
      iss: did, // issuer
      sub: did, // subject  
      aud: 'your-app', // audience
      exp: Math.floor(Date.now()/1000) + 3600, // expires
      iat: Math.floor(Date.now()/1000) // issued at
    };
  }
  
  extractCapabilities(didDoc) {
    const caps = [];
    
    if (didDoc.authentication) caps.push('authenticate');
    if (didDoc.assertionMethod) caps.push('sign');
    if (didDoc.profile) caps.push('profile');
    if (didDoc.service) caps.push('services');
    
    return caps;
  }
}

// Enhanced auth with web standards compatibility
const authResult = await didAuth.authenticate(pubkey, signature);
console.log("Enhanced auth context:", authResult);
        

🏭 Stage 4: Production Ready

Time to ship! Here are the production considerations and optimizations that will make your DID integration rock solid.

Performance Optimizations

Smart Caching Strategy

class ProductionDIDResolver {
  constructor() {
    this.cache = new Map();
    this.pendingRequests = new Map(); // Prevent duplicate requests
  }
  
  async resolve(did, options = {}) {
    // Check cache first
    const cached = this.cache.get(did);
    if (cached && !this.isExpired(cached, options.maxAge || 3600)) {
      return cached.data;
    }
    
    // Prevent duplicate requests for same DID
    if (this.pendingRequests.has(did)) {
      return this.pendingRequests.get(did);
    }
    
    // Start resolution
    const promise = this.doResolve(did, options);
    this.pendingRequests.set(did, promise);
    
    try {
      const result = await promise;
      
      // Cache with metadata
      this.cache.set(did, {
        data: result,
        timestamp: Date.now(),
        source: result._resolveSource || 'unknown'
      });
      
      return result;
    } finally {
      this.pendingRequests.delete(did);
    }
  }
  
  async doResolve(did, options) {
    // Layer 1: HTTP resolution (fastest)
    if (!options.skipHttp) {
      try {
        const httpResult = await this.resolveHttp(did);
        httpResult._resolveSource = 'http';
        return httpResult;
      } catch (e) {
        console.debug('HTTP resolution failed:', e.message);
      }
    }
    
    // Layer 2: Enhanced with relays
    if (!options.skipRelays) {
      try {
        const enhanced = await this.resolveEnhanced(did);
        enhanced._resolveSource = 'relays';
        return enhanced;
      } catch (e) {
        console.debug('Enhanced resolution failed:', e.message);
      }
    }
    
    // Layer 3: Offline fallback (always works)
    const offline = await this.resolveOffline(did);
    offline._resolveSource = 'offline';
    return offline;
  }
  
  isExpired(cached, maxAge) {
    const age = (Date.now() - cached.timestamp) / 1000;
    return age > maxAge;
  }
}
        

Batch Operations

class BatchDIDResolver extends ProductionDIDResolver {
  async resolveBatch(dids, options = {}) {
    const batchSize = options.batchSize || 10;
    const results = {};
    
    // Process in batches to avoid overwhelming servers
    for (let i = 0; i < dids.length; i += batchSize) {
      const batch = dids.slice(i, i + batchSize);
      const batchPromises = batch.map(did => 
        this.resolve(did, options).catch(e => ({ error: e.message }))
      );
      
      const batchResults = await Promise.all(batchPromises);
      batch.forEach((did, index) => {
        results[did] = batchResults[index];
      });
      
      // Small delay between batches to be nice to servers
      if (i + batchSize < dids.length) {
        await new Promise(resolve => setTimeout(resolve, 100));
      }
    }
    
    return results;
  }
}

// Resolve 100 DIDs efficiently
const resolver = new BatchDIDResolver();
const results = await resolver.resolveBatch(arrayOf100DIDs);
        

Error Handling & Monitoring

class RobustDIDResolver extends BatchDIDResolver {
  constructor(options = {}) {
    super();
    this.metrics = {
      resolutions: 0,
      cacheHits: 0,
      httpSuccess: 0,
      relaySuccess: 0,
      offlineOnly: 0,
      errors: 0
    };
    
    this.onError = options.onError || console.error;
    this.onMetrics = options.onMetrics || (() => {});
  }
  
  async resolve(did, options = {}) {
    const startTime = Date.now();
    this.metrics.resolutions++;
    
    try {
      const result = await super.resolve(did, options);
      
      // Track success metrics
      switch (result._resolveSource) {
        case 'http': this.metrics.httpSuccess++; break;
        case 'relays': this.metrics.relaySuccess++; break;
        case 'offline': this.metrics.offlineOnly++; break;
      }
      
      // Emit metrics
      this.onMetrics({
        type: 'resolution_success',
        did,
        source: result._resolveSource,
        duration: Date.now() - startTime,
        cached: this.cache.has(did)
      });
      
      return result;
    } catch (error) {
      this.metrics.errors++;
      
      this.onError({
        type: 'resolution_error',
        did,
        error: error.message,
        duration: Date.now() - startTime
      });
      
      // Always try to return something useful
      try {
        const fallback = await this.resolveOffline(did);
        fallback._resolveSource = 'error_fallback';
        return fallback;
      } catch (fallbackError) {
        // Last resort: minimal identity
        return {
          id: did,
          "@context": ["https://w3id.org/did"],
          _resolveSource: 'minimal_fallback'
        };
      }
    }
  }
  
  getMetrics() {
    return {
      ...this.metrics,
      cacheHitRate: this.metrics.cacheHits / this.metrics.resolutions,
      httpSuccessRate: this.metrics.httpSuccess / this.metrics.resolutions,
      errorRate: this.metrics.errors / this.metrics.resolutions
    };
  }
}

// Production-ready resolver with full monitoring
const resolver = new RobustDIDResolver({
  onError: (error) => console.error('DID Resolution Error:', error),
  onMetrics: (metrics) => sendToAnalytics(metrics)
});
        

Integration Checklist

✅ Before Going Live

🚀 Launch Optimization

🎓 Advanced: Beyond the Basics

Ready for advanced patterns? Here's where DIDs really shine - connecting Nostr to the broader identity ecosystem.

Verifiable Credentials (Future)

Your DID becomes the foundation for verifiable credentials - cryptographically signed claims about identity, achievements, certifications.

// Future: Issue a credential about a Nostr user
const credential = {
  "@context": ["https://www.w3.org/2018/credentials/v1"],
  "type": ["VerifiableCredential", "NostrContributorCredential"],
  "issuer": "did:nostr:issuer-pubkey...",
  "subject": "did:nostr:32e182...", // The user's DID
  "issuanceDate": "2025-01-27T00:00:00Z",
  "credentialSubject": {
    "contributorLevel": "Core Developer",
    "projectsContributed": ["nostr-protocol", "nips"],
    "badgeEarned": "nostr-og-2024"
  },
  "proof": {
    "type": "SchnorrSignature2025",
    "created": "2025-01-27T00:00:00Z",
    "verificationMethod": "did:nostr:issuer-pubkey...#key1",
    "signatureValue": "..."
  }
};

// Verifiable, portable, works across all systems
        

Enterprise Integration

Bridge Nostr identity into enterprise systems:

class EnterpriseNostIDentity {
  async authenticateEmployee(nostrPubkey) {
    const did = `did:nostr:${nostrPubkey}`;
    const didDoc = await resolver.resolve(did);
    
    // Check if employee is verified via company credential
    const employeeCredential = await this.verifyEmployeeCredential(did);
    if (!employeeCredential) {
      throw new Error("Not a verified employee");
    }
    
    // Create standard SAML/OAuth token with DID context
    return this.createEnterpriseToken({
      subject: did,
      email: didDoc.profile?.nip05,
      name: didDoc.profile?.name,
      department: employeeCredential.department,
      clearanceLevel: employeeCredential.clearanceLevel,
      // Standard enterprise claims
      groups: ['nostr-users', 'verified-employees'],
      roles: ['user', 'contributor']
    });
  }
}
        

🛠️ Utilities & Helper Functions

Copy-paste utilities to get started immediately. All functions are self-contained and work in both Node.js and browser environments.

Core Utilities

// Convert pubkey to Multikey format (required for DID documents)
function pubkeyToMultikey(pubkeyHex) {
  // Add even parity byte (0x02) to create compressed pubkey
  const compressed = '02' + pubkeyHex;
  
  // Add multicodec varint for secp256k1 (0xe7, 0x01)
  const withCodec = 'e701' + compressed;
  
  // Encode with multibase base16-lower (f prefix)
  return 'f' + withCodec;
}

// Validate DID format
function isValidNostrDID(did) {
  if (!did.startsWith('did:nostr:')) return false;
  
  const pubkey = did.replace('did:nostr:', '');
  return /^[0-9a-f]{64}$/.test(pubkey);
}

// Extract pubkey from DID
function didToPubkey(did) {
  if (!isValidNostrDID(did)) throw new Error('Invalid DID format');
  return did.replace('did:nostr:', '');
}

// Convert between npub and DID formats
function npubToDID(npub) {
  // Requires nostr-tools or similar for nip19.decode
  const { data } = nip19.decode(npub);
  return `did:nostr:${data}`;
}

function didToNpub(did) {
  const pubkey = didToPubkey(did);
  return nip19.npubEncode(pubkey);
}

// Generate timestamp for profiles
function getCurrentTimestamp() {
  return Math.floor(Date.now() / 1000);
}

// Check if profile data is fresh
function isProfileFresh(profile, maxAgeSeconds = 3600) {
  if (!profile?.timestamp) return false;
  const age = getCurrentTimestamp() - profile.timestamp;
  return age < maxAgeSeconds;
}
        

HTTP Resolution Helpers

// Build .well-known URL for a DID
function buildWellKnownURL(did, domain = 'example.com') {
  const pubkey = didToPubkey(did);
  return `https://${domain}/.well-known/did/nostr/${pubkey}.json`;
}

// Fetch DID document via HTTP with proper error handling
async function fetchDIDDocument(did, domain) {
  const url = buildWellKnownURL(did, domain);
  
  try {
    const response = await fetch(url, {
      headers: {
        'Accept': 'application/json',
        'User-Agent': 'DID-Nostr-Resolver/1.0'
      },
      // Reasonable timeouts
      timeout: 5000
    });
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    // Check freshness via headers
    const timestamp = response.headers.get('Nostr-Timestamp');
    const lastModified = response.headers.get('Last-Modified');
    
    const didDoc = await response.json();
    
    // Add metadata for caching decisions
    didDoc._metadata = {
      fetchedAt: Date.now(),
      source: 'http',
      timestamp: timestamp ? parseInt(timestamp) : null,
      lastModified: lastModified,
      url: url
    };
    
    return didDoc;
  } catch (error) {
    throw new Error(`HTTP resolution failed: ${error.message}`);
  }
}

// Serve DID document via HTTP (for servers)
function serveDIDDocument(req, res, didDoc) {
  const timestamp = didDoc.profile?.timestamp || getCurrentTimestamp();
  
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('Nostr-Timestamp', timestamp.toString());
  res.setHeader('Last-Modified', new Date(timestamp * 1000).toUTCString());
  res.setHeader('Cache-Control', 'public, max-age=3600');
  res.setHeader('Access-Control-Allow-Origin', '*');
  
  res.json(didDoc);
}
        

Basic Testing

// Simple tests for your DID integration
const testDID = "did:nostr:32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245";

// Test basic format
console.assert(isValidNostrDID(testDID), "DID should be valid");

// Test offline resolution
const didDoc = resolveDidOffline(testDID);
console.assert(didDoc.id === testDID, "Document ID should match");

// Test utilities
const pubkey = didToPubkey(testDID);
console.assert(pubkey.length === 64, "Pubkey should be 64 hex chars");

console.log("✅ Basic tests passed");
        

🔄 Migration: Adding DIDs to Existing Apps

Step-by-step guide to add DID support to your existing Nostr application without breaking anything. Each step adds value while maintaining compatibility.

Phase 1: Passive Integration (1 hour)

Add DID support without changing existing functionality:

// 1. Add DID utilities to your existing codebase
import { pubkeyToMultikey, isValidNostrDID, didToPubkey } from './did-utils.js';

// 2. Extend your existing user object
class User {
  constructor(pubkey) {
    this.pubkey = pubkey;
    this.npub = nip19.npubEncode(pubkey);
    this.did = `did:nostr:${pubkey}`; // ← NEW: Add DID
  }
  
  // Your existing methods work unchanged
  async getProfile() {
    return await fetchProfileFromRelays(this.pubkey);
  }
  
  // NEW: Optional DID-enhanced profile
  async getDIDProfile() {
    try {
      const didDoc = await smartResolve(this.did);
      return didDoc.profile || await this.getProfile(); // Fallback
    } catch (error) {
      return await this.getProfile(); // Always fallback
    }
  }
}

// 3. Your existing code works unchanged
const user = new User("32e182...");
const profile = await user.getProfile(); // Still works

// 4. New code can use DID features
const enhancedProfile = await user.getDIDProfile(); // Enhanced
        

Phase 2: Gradual Enhancement (1 day)

Start using DIDs for new features while keeping existing flows:

// 1. Enhance your profile manager
class ProfileManager {
  async getProfile(pubkey, options = {}) {
    // Check if DID enhancement is requested
    if (options.useDID) {
      const did = `did:nostr:${pubkey}`;
      const didDoc = await smartResolve(did);
      
      if (didDoc.profile && isProfileFresh(didDoc.profile)) {
        return {
          ...didDoc.profile,
          source: 'did',
          enhanced: true
        };
      }
    }
    
    // Fallback to existing relay-based approach
    return await this.fetchFromRelays(pubkey);
  }
  
  // NEW: Batch profile loading with DIDs
  async getProfiles(pubkeys) {
    const dids = pubkeys.map(pk => `did:nostr:${pk}`);
    const didDocs = await resolver.resolveBatch(dids);
    
    return pubkeys.map(pubkey => {
      const did = `did:nostr:${pubkey}`;
      const didDoc = didDocs[did];
      
      if (didDoc?.profile && isProfileFresh(didDoc.profile)) {
        return { pubkey, ...didDoc.profile, source: 'did' };
      } else {
        // Queue for relay fetch
        return { pubkey, source: 'pending' };
      }
    });
  }
}

// 2. Use in UI components
async function ProfileCard({ pubkey }) {
  const [profile, setProfile] = useState(null);
  
  useEffect(() => {
    // Try DID first, fallback gracefully
    profileManager.getProfile(pubkey, { useDID: true })
      .then(setProfile)
      .catch(() => {
        // Fallback to old method
        return profileManager.getProfile(pubkey, { useDID: false });
      })
      .then(setProfile);
  }, [pubkey]);
  
  return (
    <div className="profile-card">
      <img src={profile?.picture} alt={profile?.name || "Unknown"} />
      <h3>{profile?.name || truncatePubkey(pubkey)}</h3>
      <p>{profile?.about}</p>
      {profile?.source === 'did' && <span className="badge">Fast Load</span>}
    </div>
  );
}
        

Phase 3: Full Integration (2-3 days)

Make DIDs the primary identity system with relay fallback:

// 1. DID-first identity system
class IdentitySystem {
  constructor() {
    this.resolver = new RobustDIDResolver();
    this.cache = new IdentityCache();
  }
  
  async resolveIdentity(identifier) {
    let did, pubkey;
    
    // Handle multiple input formats
    if (identifier.startsWith('did:nostr:')) {
      did = identifier;
      pubkey = didToPubkey(did);
    } else if (identifier.startsWith('npub')) {
      pubkey = nip19.decode(identifier).data;
      did = `did:nostr:${pubkey}`;
    } else if (/^[0-9a-f]{64}$/.test(identifier)) {
      pubkey = identifier;
      did = `did:nostr:${pubkey}`;
    } else {
      throw new Error('Invalid identifier format');
    }
    
    // Resolve with full caching
    const identity = await this.cache.get(did);
    if (identity && isProfileFresh(identity.profile)) {
      return identity;
    }
    
    // Full resolution
    const didDoc = await this.resolver.resolve(did);
    const identity = {
      did,
      pubkey,
      npub: nip19.npubEncode(pubkey),
      profile: didDoc.profile,
      verificationMethods: didDoc.verificationMethod,
      services: didDoc.service || [],
      capabilities: this.extractCapabilities(didDoc),
      lastUpdated: Date.now()
    };
    
    await this.cache.set(did, identity);
    return identity;
  }
  
  extractCapabilities(didDoc) {
    const caps = new Set();
    
    if (didDoc.authentication) caps.add('authenticate');
    if (didDoc.assertionMethod) caps.add('sign');
    if (didDoc.keyAgreement) caps.add('encrypt');
    if (didDoc.service?.find(s => s.type === 'Relay')) caps.add('nostr');
    if (didDoc.profile) caps.add('profile');
    
    return Array.from(caps);
  }
}

// 2. Update your main app
class NostrApp {
  constructor() {
    this.identity = new IdentitySystem();
    this.users = new Map(); // Local user cache
  }
  
  async loadUser(identifier) {
    try {
      // DID-first approach
      const identity = await this.identity.resolveIdentity(identifier);
      
      const user = {
        ...identity,
        // Add app-specific methods
        async sendNote(content) {
          // Your existing note sending logic
        },
        
        async getTimeline() {
          // Enhanced timeline with DID context
          const relays = identity.services
            .filter(s => s.type === 'Relay')
            .map(s => s.serviceEndpoint);
          
          return await fetchTimelineFromRelays(identity.pubkey, relays);
        }
      };
      
      this.users.set(identity.did, user);
      return user;
    } catch (error) {
      console.error('Failed to load user identity:', error);
      throw error;
    }
  }
}

// 3. Your app now works with any identifier format
const app = new NostrApp();

// All of these work:
const user1 = await app.loadUser("did:nostr:32e182...");
const user2 = await app.loadUser("npub1xt0c5e34g2twm...");  
const user3 = await app.loadUser("32e1827635450ebb...");
        

Phase 4: Advanced Features (ongoing)

Add cutting-edge DID features as they become available:

❓ FAQ: Common Questions

General Questions

Why should I care about DIDs if Nostr already works?

DIDs make your Nostr identity work everywhere, not just in Nostr apps. Think of it like having a driver's license that works in every country - same identity, universal recognition. Plus you get massive performance wins from HTTP caching.

Do I need to change my existing Nostr app?

Not at all! DIDs are additive. Your existing code keeps working. You can add DID support gradually, starting with just better profile caching.

What's the performance impact?

Massive improvement. HTTP resolution can be 100x faster than relay queries. Offline resolution is instant. Your users will notice the speed boost immediately.

Is this just another crypto thing I need to learn?

Nope! DIDs are web standards from W3C (same folks who made HTML). If you know HTTP and JSON, you know 90% of what you need. The crypto part is just your existing Nostr keys.

Technical Questions

What happens if the HTTP endpoint is down?

Graceful degradation. The resolver tries HTTP first, then relays, then offline generation. Something always works.

How do I handle profile updates?

The timestamp field tells you when the profile was last updated. Compare it with your local cache to know if you have the latest data.

Can I use this in React Native / mobile apps?

Absolutely! Offline resolution works great on mobile. HTTP resolution reduces battery drain from WebSocket connections.

What about privacy?

Same as Nostr - profile info is already public. DIDs just make it more accessible. You control what goes in the profile field, same as with Nostr events.

How do I serve .well-known endpoints?

Just static JSON files! Put them at /.well-known/did/nostr/{pubkey}.json on your web server. Works with GitHub Pages, Netlify, any hosting.

Advanced Questions

Can I extend DID documents with custom fields?

Yes! Add your own fields alongside the standard ones. Just make sure they don't conflict with standard DID fields.

How do I handle multiple keys per user?

Each Nostr pubkey gets its own DID. If users have multiple keys, they get multiple DIDs. You can link them via service endpoints or credentials.

What about key rotation?

Nostr doesn't support key rotation, so neither do Nostr DIDs. If someone's key is compromised, they need a new DID (same as a new Nostr identity).

Can I use this with Lightning/payments?

Yes! Put Lightning addresses in the profile field, or create service endpoints for payment methods. Perfect for Lightning address discovery.

📚 Resources & Next Steps

Code Examples Repository

Find complete, runnable examples for all the patterns in this guide:

📁 GitHub Examples Repository

Tools & Libraries

Community & Support

Specification Documents

Your Next Steps

  1. Try the Quick Win - Convert one of your pubkeys to a DID (2 minutes)
  2. Add Offline Resolution - Implement basic DID document generation (30 minutes)
  3. Enhance One Feature - Add DID support to profile loading (1 hour)
  4. Serve HTTP Endpoints - Set up .well-known hosting (2 hours)
  5. Production Deploy - Launch with full caching and monitoring (1 day)
  6. Share Your Experience - Help improve this guide with your feedback!