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.
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.
You know Nostr events. DIDs use "DID Documents" - think of them as persistent, structured profiles that web services can discover and trust.
{
"kind": 0,
"created_at": 1737906600,
"content": "{\"name\":\"Alice\",\"about\":\"Building nostr\",\"picture\":\"https://...\"}",
"pubkey": "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
"sig": "..."
}
{
"@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
}
}
| Aspect | Nostr Event | DID 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 |
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.
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);
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...");
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...");
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...");
Now let's integrate DIDs into real applications. Here are the most common patterns that work great with existing Nostr apps.
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...");
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);
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);
Time to ship! Here are the production considerations and optimizations that will make your DID integration rock solid.
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;
}
}
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);
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)
});
Ready for advanced patterns? Here's where DIDs really shine - connecting Nostr to the broader identity ecosystem.
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
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']
});
}
}
Copy-paste utilities to get started immediately. All functions are self-contained and work in both Node.js and browser environments.
// 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;
}
// 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);
}
// 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");
Step-by-step guide to add DID support to your existing Nostr application without breaking anything. Each step adds value while maintaining compatibility.
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
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>
);
}
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...");
Add cutting-edge DID features as they become available:
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.
Not at all! DIDs are additive. Your existing code keeps working. You can add DID support gradually, starting with just better profile caching.
Massive improvement. HTTP resolution can be 100x faster than relay queries. Offline resolution is instant. Your users will notice the speed boost immediately.
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.
Graceful degradation. The resolver tries HTTP first, then relays, then offline generation. Something always works.
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.
Absolutely! Offline resolution works great on mobile. HTTP resolution reduces battery drain from WebSocket connections.
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.
Just static JSON files! Put them at /.well-known/did/nostr/{pubkey}.json
on your web server. Works with GitHub Pages, Netlify, any hosting.
Yes! Add your own fields alongside the standard ones. Just make sure they don't conflict with standard DID fields.
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.
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).
Yes! Put Lightning addresses in the profile field, or create service endpoints for payment methods. Perfect for Lightning address discovery.
Find complete, runnable examples for all the patterns in this guide: