npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nostr-dev-kit/sessions

v1.0.0

Published

Framework-agnostic session management for NDK with multi-account support

Readme

@nostr-dev-kit/sessions

Framework-agnostic session management for NDK with multi-account support and persistence.

Features

  • 🔐 Multi-account support - Manage multiple Nostr accounts simultaneously
  • 💾 Flexible persistence - Built-in localStorage, filesystem, and memory storage
  • 🔄 Auto-sync - Automatically fetch follows, mutes, relays, and events
  • 🎯 Framework-agnostic - Works with React, Svelte, Vue, vanilla JS, Node.js, etc.
  • 🔌 Minimal boilerplate - Simple, intuitive API
  • 🎨 Full TypeScript - Complete type safety

Installation

npm install @nostr-dev-kit/sessions
# or
bun add @nostr-dev-kit/sessions

Quick Start

import NDK from '@nostr-dev-kit/ndk';
import { NDKSessionManager, MemoryStorage } from '@nostr-dev-kit/sessions';
import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';

// Setup NDK
const ndk = new NDK({ explicitRelayUrls: ['wss://relay.damus.io'] });
await ndk.connect();

// Create session manager
const sessions = new NDKSessionManager(ndk);

// Login
const signer = new NDKPrivateKeySigner(myNsec);
await sessions.login(signer, {
  follows: true,    // Auto-fetch follows
  mutes: true,      // Auto-fetch mutes
  setActive: true   // Set as active session
});

console.log('Active user:', sessions.activeUser);

Usage Examples

Browser with localStorage

import { NDKSessionManager, LocalStorage } from '@nostr-dev-kit/sessions';

const sessions = new NDKSessionManager(ndk, {
  storage: new LocalStorage(),
  autoSave: true  // Automatically persist changes
});

// Restore sessions from localStorage
await sessions.restore();

// Login with browser extension (NIP-07)
import { NDKNip07Signer } from '@nostr-dev-kit/ndk';
const signer = new NDKNip07Signer();
await sessions.login(signer);

Node.js/CLI with File Storage

import { NDKSessionManager, FileStorage } from '@nostr-dev-kit/sessions';

const sessions = new NDKSessionManager(ndk, {
  storage: new FileStorage('./.ndk-sessions.json'),
  autoSave: true
});

// Restore sessions from file
await sessions.restore();

// Login
const signer = new NDKPrivateKeySigner(process.env.NOSTR_NSEC);
await sessions.login(signer, {
  follows: true,
  mutes: true
});

// Use active session
const event = new NDKEvent(ndk, { kind: 1, content: 'Hello from CLI!' });
await event.publish(); // Uses active session's signer

Multi-Account Management

// Login multiple accounts
const signer1 = new NDKPrivateKeySigner(nsec1);
const signer2 = new NDKPrivateKeySigner(nsec2);

const pubkey1 = await sessions.login(signer1);
const pubkey2 = await sessions.login(signer2);

// Get all sessions
console.log('Sessions:', sessions.getSessions());

// Switch between accounts
sessions.switchTo(pubkey1);
console.log('Active:', sessions.activePubkey);

sessions.switchTo(pubkey2);
console.log('Active:', sessions.activePubkey);

// Logout specific account
sessions.logout(pubkey1);

// Or logout active account
sessions.logout();

Read-Only Sessions

import { NDKUser } from '@nostr-dev-kit/ndk';

// Create read-only session without signer
const user = ndk.getUser({ pubkey: somePubkey });
await sessions.login(user, {
  follows: true,
  relayList: true
});

// User data is fetched and cached, but can't sign events

Subscribe to Changes

const unsubscribe = sessions.subscribe((state) => {
  console.log('Active user:', state.activePubkey);
  console.log('Sessions count:', state.sessions.size);
});

// Later...
unsubscribe();

Auto-Fetch Session Data

await sessions.login(signer, {
  // Fetch contact list (kind 3)
  follows: true,

  // Fetch mute list (kind 10000)
  mutes: true,

  // Fetch blocked relay list (kind 10001)
  blockedRelays: true,

  // Fetch user's relay list (kind 10002)
  relayList: true,

  // Fetch NIP-60 wallet (kind 17375)
  wallet: true,

  // Fetch additional event kinds
  events: new Map([
    [30078, undefined], // NIP-78 app data
  ]),

  // Automatically set muteFilter on NDK (default: true)
  setMuteFilter: true,

  // Automatically set relayConnectionFilter on NDK (default: true)
  setRelayConnectionFilter: true
});

// Access session data
const session = sessions.activeSession;
console.log('Following:', session.followSet?.size, 'users');
console.log('Muted:', session.muteSet?.size, 'items');
console.log('Relay list:', session.relayList);
console.log('App data:', session.events.get(30078));

Custom Event Classes

You can provide custom NDKEvent subclasses to automatically wrap events as they're received. This is useful for adding custom methods or properties to specific event kinds:

import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { NDKBlossomList } from '@nostr-dev-kit/ndk';

// Define a custom event class for kind 10015 (Interest Lists)
class NDKInterestList extends NDKEvent {
  static kinds = [10015 as NDKKind]; // Required for eventConstructors option

  static from(event: NDKEvent): NDKInterestList {
    return new NDKInterestList(event.ndk, event.rawEvent());
  }

  get interests(): string[] {
    return this.getMatchingTags('t').map(tag => tag[1]);
  }

  addInterest(interest: string): void {
    this.tags.push(['t', interest]);
  }
}

// Option 1: Using eventConstructors (recommended - more ergonomic)
await sessions.login(signer, {
  follows: true,
  eventConstructors: [NDKBlossomList, NDKInterestList]
});

// Option 2: Using events Map (explicit)
await sessions.login(signer, {
  follows: true,
  events: new Map([
    [10015, NDKInterestList], // Wrap kind 10015 with custom class
    [30078, undefined],        // Fetch kind 30078 as regular NDKEvent
  ])
});

// Access the wrapped event with custom methods
const session = sessions.activeSession;
const interestList = session.events.get(10015) as NDKInterestList;
console.log('User interests:', interestList.interests);

// Use custom methods
interestList.addInterest('nostr');
await interestList.publish();

Key features:

  • Automatic wrapping - Events are wrapped using the class's .from() method
  • Type safety - Cast to your custom class for full TypeScript support
  • Custom methods - Add domain-specific methods and computed properties
  • Inheritance - Custom classes extend NDKEvent, so all standard methods work
  • Ergonomic API - Use eventConstructors array for cleaner syntax

Requirements:

  • Custom class must extend NDKEvent
  • Must implement a static from(event: NDKEvent) method
  • Must have a static kinds array property (when using eventConstructors)
  • The from() method should return an instance of your custom class

eventConstructors vs events:

  • eventConstructors - More ergonomic, uses the class's static kinds property
  • events - More explicit, requires manual kind-to-class mapping
  • Both options can be used together and will be merged

Manual Persistence

import { MemoryStorage } from '@nostr-dev-kit/sessions';

const sessions = new NDKSessionManager(ndk, {
  storage: new MemoryStorage(),
  autoSave: false  // Disable auto-save
});

// Manually persist
await sessions.persist();

// Manually restore
await sessions.restore();

// Clear storage
await sessions.clear();

Runtime Wallet Control

Sessions supports runtime control of wallet fetching, allowing apps to enable/disable wallet features based on user preferences:

// Start session without wallet
const sessions = new NDKSessionManager(ndk, {
  fetches: { follows: true }
});

await sessions.login(signer);

// Later, when user wants to use wallet features
if (!sessions.isWalletEnabled()) {
  // App shows UI prompt: "Enable wallet for zaps?"
  const userConsent = await askUser();

  if (userConsent) {
    // Enable wallet and remember preference
    sessions.enableWallet();
  }
}

// Check wallet state
console.log('Wallet enabled:', sessions.isWalletEnabled());

// Disable wallet
sessions.disableWallet();

Key features:

  • Runtime control - Enable/disable wallet after session starts
  • Persistent preferences - User's choice is saved and restored
  • Per-session - Each account can have different wallet settings

Usage patterns:

// 1. Wallet app (always enable)
const sessions = new NDKSessionManager(ndk, {
  fetches: { wallet: true }
});

// 2. Social app (ask user first time)
const sessions = new NDKSessionManager(ndk, {
  fetches: { wallet: false } // disabled by default
});
await sessions.restore(); // respects saved preferences

// When user tries to zap
if (!sessions.isWalletEnabled()) {
  if (await confirm("Enable wallet?")) {
    sessions.enableWallet(); // saves preference
  }
}

// 3. Multi-account with per-user preferences
sessions.enableWallet(userPubkey1);  // enable for user 1
sessions.disableWallet(userPubkey2); // disable for user 2

API Reference

NDKSessionManager

Constructor

new NDKSessionManager(ndk: NDK, options?: SessionManagerOptions)

Options:

  • storage?: SessionStorage - Storage backend (LocalStorage, FileStorage, MemoryStorage, or custom)
  • autoSave?: boolean - Auto-persist on changes (default: true)
  • saveDebounceMs?: number - Debounce time for auto-save (default: 500)

Methods

createAccount(options?): Promise<{ pubkey: Hexpubkey; signer: NDKPrivateKeySigner }>

Create a new Nostr account with a generated private key and optional initial setup.

Options:

  • profile?: NDKUserProfile - Profile metadata to publish as kind:0 (name, about, picture, etc.)
  • relays?: string[] - Relay URLs to publish as NIP-65 relay list (kind:10002)
  • wallet?: { mints: string[], relays?: string[] } - NIP-60 wallet configuration
    • mints - Array of mint URLs for Cashu wallet
    • relays - Optional relay URLs for wallet events (defaults to main relays if not specified)

Returns: Object containing the new account's pubkey and signer.

// Create bare account
const { pubkey, signer } = await sessions.createAccount();
console.log('New account created:', pubkey);
console.log('Private key:', signer.privateKey);

// Create account with profile
const { pubkey } = await sessions.createAccount({
  profile: {
    name: 'Alice',
    about: 'Hello Nostr!',
    picture: 'https://example.com/avatar.jpg'
  }
});

// Create account with relay list
const { pubkey } = await sessions.createAccount({
  relays: [
    'wss://relay.damus.io',
    'wss://relay.primal.net'
  ]
});

// Create account with wallet
const { pubkey } = await sessions.createAccount({
  wallet: {
    mints: ['https://mint.minibits.cash'],
    relays: ['wss://relay.damus.io']
  }
});

// Create fully configured account
const { pubkey, signer } = await sessions.createAccount({
  profile: { name: 'Alice', about: 'New to Nostr' },
  relays: ['wss://relay.damus.io', 'wss://relay.primal.net'],
  wallet: {
    mints: ['https://mint.minibits.cash'],
    relays: ['wss://relay.damus.io']
  }
});

// Save the nsec for the user
console.log('Your private key (keep safe!):', signer.privateKey);

Notes:

  • The account is automatically logged in and set as active
  • Profile is published as kind:0 event
  • Relays are published as kind:10002 event (NIP-65)
  • Wallet creates both kind:17375 (wallet config) and kind:10019 (backup) events
  • Returns the signer so you can show the user their private key for backup

login(userOrSigner, options?): Promise<Hexpubkey>

Login with an existing signer or user.

Options:

  • follows?: boolean | NDKKind[] - Fetch contact list (kind 3) and optional kind-scoped follows
  • mutes?: boolean - Fetch mute list (kind 10000)
  • blockedRelays?: boolean - Fetch blocked relay list (kind 10001)
  • relayList?: boolean - Fetch user's relay list (kind 10002)
  • wallet?: boolean - Fetch NIP-60 wallet (kind 17375)
  • events?: Map<NDKKind, any> - Fetch specific replaceable event kinds. Value can be:
    • A custom NDKEvent subclass with a static from() method to wrap events
    • undefined to fetch as regular NDKEvent
  • eventConstructors?: NDKEventConstructor[] - Array of event class constructors to register. More ergonomic alternative to events. Each constructor must have:
    • A static kinds array property listing the kinds it handles
    • A static from(event: NDKEvent) method to wrap events
  • setMuteFilter?: boolean - Set muteFilter on NDK based on session's mute data (default: true)
  • setRelayConnectionFilter?: boolean - Set relayConnectionFilter on NDK based on session's blocked relays (default: true)
  • setActive?: boolean - Set this session as active immediately
// Using eventConstructors (recommended)
await sessions.login(signer, {
  follows: true,
  mutes: true,
  relayList: true,
  wallet: true,
  eventConstructors: [NDKBlossomList, CustomInterestList],
  setActive: true
});

// Using events Map (explicit)
await sessions.login(signer, {
  follows: true,
  mutes: true,
  relayList: true,
  wallet: true,
  events: new Map([
    [10015, CustomInterestList], // Wrap with custom class
    [30078, undefined]            // Regular NDKEvent
  ]),
  setActive: true
});

logout(pubkey?): void

Logout (remove) a session. If no pubkey provided, logs out active session.

switchTo(pubkey): void

Switch to a different session.

restore(): Promise<void>

Restore sessions from storage.

persist(): Promise<void>

Persist sessions to storage.

clear(): Promise<void>

Clear all sessions from storage.

subscribe(callback): UnsubscribeFn

Subscribe to state changes.

destroy(): void

Cleanup and stop all subscriptions.

enableWallet(pubkey?): void

Enable wallet fetching for a session. If no pubkey provided, uses active session. Updates preference and restarts subscription with wallet enabled.

disableWallet(pubkey?): void

Disable wallet fetching for a session. If no pubkey provided, uses active session. Updates preference and restarts subscription without wallet.

isWalletEnabled(pubkey?): boolean

Check if wallet fetching is enabled for a session. If no pubkey provided, uses active session. Returns saved preference or false if not set.

Properties

activeSession: NDKSession | undefined

Get the active session.

activeUser: NDKUser | undefined

Get the active user.

activePubkey: Hexpubkey | undefined

Get the active pubkey.

getSessions(): Map<Hexpubkey, NDKSession>

Get all sessions.

getSession(pubkey): NDKSession | undefined

Get a specific session.

Storage Implementations

LocalStorage

Browser localStorage implementation.

new LocalStorage(key?: string)

FileStorage

Node.js filesystem implementation.

new FileStorage(filePath?: string)

MemoryStorage

In-memory implementation (no persistence).

new MemoryStorage()

Custom Storage

Implement the SessionStorage interface:

interface SessionStorage {
  save(sessions: Map<Hexpubkey, SerializedSession>, activePubkey?: Hexpubkey): Promise<void>;
  load(): Promise<{ sessions: Map<Hexpubkey, SerializedSession>; activePubkey?: Hexpubkey }>;
  clear(): Promise<void>;
}

Framework Integration

React (with ndk-hooks)

// Coming soon: useNDKSessions hook in @nostr-dev-kit/react

Svelte 5 (with svelte)

// Coming soon: sessions store in @nostr-dev-kit/svelte

Vanilla Store Access

import { createSessionStore } from '@nostr-dev-kit/sessions';

const store = createSessionStore();
store.getState().init(ndk);

// Subscribe to changes
const unsubscribe = store.subscribe((state) => {
  console.log(state.activePubkey);
});

Best Practices

Security

⚠️ WARNING: Signer serialization stores private keys. In production:

  1. Use encrypted storage
  2. Never commit .ndk-sessions.json to version control
  3. Use environment variables for sensitive keys
  4. Consider NIP-07 (browser extension) or NIP-46 (remote signer) for better security

Performance

  • Use autoSave: true with appropriate saveDebounceMs to avoid excessive writes
  • Call destroy() when cleaning up to prevent memory leaks
  • Use MemoryStorage for testing or when persistence isn't needed

Error Handling

try {
  await sessions.login(signer);
} catch (error) {
  console.error('Login failed:', error);
}

try {
  await sessions.restore();
} catch (error) {
  console.error('Failed to restore sessions:', error);
}

License

MIT