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

ndk-vue

v0.0.2

Published

Simple NDK bindings for Vue

Readme

@nostr-dev-kit/vue

Vue 3 Integration for NDK

A simple conversion of the @nostr-dev-kit/ndk-svelte framework integration.

⚠️ WARNING: This integration has not been tested and the documentation may be inaccurate.


🔴 Critical API Note for AI Assistants

The $subscribe() method takes a CALLBACK FUNCTION, not direct config:

// ✅ CORRECT - callback function returning config
const notes = ndk.$subscribe(() => ({
  filters: [{ kinds: [1], limit: 50 }],
}))

// ❌ WRONG - direct config (this API doesn't exist)
const notes = ndk.$subscribe({ kinds: [1], limit: 50 })
const notes = ndk.$subscribe([{ kinds: [1] }])

Status

Current Version: 0.0.2 Status: Alpha - Test cases passed. Real world testing in progress.

✅ Implemented Features

  • Core Subscriptions: Reactive EventSubscription with automatic cleanup
  • Namespaced Stores: Sessions, WoT, wallet, and pool
  • Wallet Integration: Full support for Cashu, NWC, and WebLN wallets
  • Nutzap Monitoring: Automatic nutzap detection and redemption
  • Test Coverage: 156 tests passing across all core functionality

🚧 Coming Soon

  • Better documentation lol

Installation

bun add ndk-vue
pnpm add ndk-vue

Setup

Initialize NDK in your app:

// lib/ndk.ts
import { createNDK } from 'ndk-vue'
import NDKCacheDexie from '@nostr-dev-kit/cache-dexie'

export const ndk = createNDK({
  explicitRelayUrls: ['wss://relay.damus.io', 'wss://relay.nostr.band'],
  cacheAdapter: new NDKCacheDexie({ dbName: 'my-app' }),
  // Enable sessions for wallet, follows, mutes, and WoT
  session: true,
})

ndk.connect()

Type-Safe Session Stores

When you enable session, TypeScript automatically knows that $wallet, $sessions, and $wot exist:

// ✅ No optional chaining needed - TypeScript knows these exist
const balance = ndk.$wallet.balance.value;
const follows = ndk.$sessions.follows;
const score = ndk.$wot.getScore(pubkey);

// Without sessions, TypeScript enforces optional chaining
const ndkNoSession = createNDK({ explicitRelayUrls: [...] });
const balance = ndkNoSession.$wallet?.balance.value; // Must use ?. operator

This is achieved through function overloads that provide compile-time guarantees about store availability.

Session Persistence: Sessions are automatically persisted to localStorage by default. Users stay logged in across page reloads.

Core Concepts

1. Reactive Subscriptions

The heart of reactivity is the $subscribe() method - a reactive, self-managing subscription.

IMPORTANT: $subscribe() takes a callback function that returns your config. This enables reactive filters and conditional subscriptions.

<script setup lang="ts">
  import { ndk } from '$lib/ndk'
  import { NDKKind } from '@nostr-dev-kit/ndk'

  // Create a reactive subscription - note the callback function () => ({ ... })
  const notes = ndk.$subscribe(() => ({
    filters: [{ kinds: [NDKKind.Text], limit: 50 }],
  }))

  // Properties are $state runes that automatically trigger reactivity
  // when accessed in Svelte templates or $effect blocks
  $inspect(notes.events) // Array of events (reactive)
  $inspect(notes.eosed) // EOSE flag (reactive)
  $inspect(notes.count) // Event count (derived)
</script>

<template>
  <article v-for="note in notes.events">
    {{ note.content }}
  </article>

  <p v-if="notes.isEmpty">No notes yet</p>
</template>

Automatic reactivity: The events, eosed, error, status, and refCount properties are $state runes that automatically trigger reactivity when events arrive and the UI accesses them.

Automatic cleanup: The subscription stops when the component unmounts. No manual cleanup needed.

2. Namespaced Stores

All stores are namespaced under the NDK instance:

import { ndk } from '$lib/ndk'

// Session management
const currentUser = ndk.$sessions.current
await ndk.$sessions.login(signer)
ndk.$sessions.logout()

// Web of Trust
await ndk.$wot.load()
const score = ndk.$wot.getScore(pubkey)

// Wallet
ndk.$wallet.set(myWallet)
const balance = ndk.$wallet.balance.value

// Pool
const connected = ndk.pool.connectedCount

Subscription API

⚠️ Documentation Update Notice: Many examples in this README show outdated syntax (without callback functions). The correct API requires a callback: ndk.$subscribe(() => ({ filters: [...] })). See the updated examples above and in the template projects for correct usage.

Basic Usage

import { ndk } from '$lib/ndk'

const sub = ndk.$subscribe(() => ({
  filters: [{ kinds: [1], authors: [pubkey], limit: 100 }],
}))

// The subscription has reactive properties
sub.events // T[] - sorted by created_at desc
sub.eosed // boolean
sub.count // number (derived)
sub.isEmpty // boolean (derived)

With Options

const highlights = ndk.$subscribe(
  { kinds: [9802], limit: 50 },
  {
    // Buffer events for performance (default: 30ms)
    bufferMs: 30,

    // Convert events to specific class
    eventClass: NDKHighlight,

    // Relay set
    relaySet: myRelaySet,

    // Callbacks
    onEvent: (event) => console.log('New event', event),
    onEose: () => console.log('EOSE reached'),
  },
)

Manual Control

const sub = ndk.$subscribe([filters], { autoStart: false })

// Manual control
sub.start()
sub.stop()
sub.restart()
sub.clear() // Clear events and restart

// Change filters
sub.changeFilters([newFilters])

// Add/remove individual events
sub.add(event)
sub.remove(eventId)

Session Management

Built-in multi-user session support with automatic persistence:

<script setup lang="ts">
  import { ndk } from '$lib/ndk'
  import { NDKNip07Signer } from '@nostr-dev-kit/ndk'

  // Fetch profile reactively
  const profile = ref()
  watch(ndk.$currentUser, (currentUser) => {
    if (currentUser?.pubkey) {
      currentUser.fetchProfile().then((p) => (profile.value = p))
    }
  })

  async function login() {
    const signer = new NDKNip07Signer()
    await ndk.$sessions.login(signer)
  }
</script>

<template>
  <div v-if="ndk.$currentSession">
    <p v-if="profile">Logged in as {{ profile.name || 'Anonymous' }}</p>
    <p v-else>Loading profile...</p>

    <p>Following {{ ndk.$follows.length }} accounts</p>
    <button @click="ndk.$sessions.logout()">Logout</button>
  </div>

  <button v-else @click="login">Login</button>
</template>

Session API

// Reactive getters on ndk
ndk.$currentSession        // NDKSession | undefined - alias for ndk.$sessions.current
ndk.$currentUser           // NDKUser | undefined - alias for ndk.$sessions.currentUser
ndk.$currentPubkey         // Hexpubkey | undefined - current user's pubkey
ndk.$follows               // ReactiveFollows (array) - current session's follow list as array
                           // Includes add(), remove(), and has() methods

// Reactive getters on ndk.$sessions
ndk.$sessions.current      // NDKSession | undefined
ndk.$sessions.currentUser  // NDKUser | undefined
ndk.$sessions.follows      // FollowsProxy (Set-like) - with add/remove/has methods
ndk.$sessions.mutes        // Map<string, string>
ndk.$sessions.mutedWords   // Set<string>
ndk.$sessions.blockedRelays // Set<string>
ndk.$sessions.relayList    // Map<string, { read: boolean; write: boolean }>
ndk.$sessions.all          // NDKSession[]

// Methods
await ndk.$sessions.login(signer, options?) // options: { setActive?: boolean }
await ndk.$sessions.add(signer)
await ndk.$sessions.switch(pubkey)
ndk.$sessions.logout(pubkey?)
ndk.$sessions.logoutAll()
ndk.$sessions.get(pubkey)
ndk.$sessions.getSessionEvent(kind)
ndk.$sessions.walletEvent  // Get NIP-60 wallet event

// Login options
{
  setActive?: boolean       // Set as active session (default: true)
}

// Note: What to fetch (follows, mutes, wallet, etc.) is configured once
// at the NDKSvelte level, not per-login. All sessions fetch the same data.

Using ndk.$follows

The $follows getter provides convenient array access to your follow list with add/remove/has methods:

<script setup lang="ts">
// Use as an array
const follows = ndk.$follows

// use in subscriptions
const feed = ndk.$subscribe(() => ({
  filters: [{ kinds: [1], authors: ndk.$follows, limit: 50 }],
}))

// Check if following (O(1) lookup)
const isFollowing = ndk.$follows.has(pubkey)

// Add/remove follows
async function followUser(pubkey: string) {
  await ndk.$follows.add(pubkey)
}

async function unfollowUser(pubkey: string) {
  await ndk.$follows.remove(pubkey)
}
</script>

<!-- Iterate over follows -->
<UserCard v-for="ndk.$follows in pubkey" :key="pubkey" />

Difference between ndk.$follows and ndk.$sessions.follows:

  • ndk.$follows - Reactive array (extends Array) with add()/remove()/has() methods. Best for templates and subscriptions.
  • ndk.$sessions.follows - FollowsProxy (Set-like) with add()/remove()/has() methods. Best when you need Set operations.

Both update reactively and both have add()/remove()/has() methods that publish to the network (except has() which is read-only).


### Automatic Wallet Loading

Sessions automatically fetch and load wallets (configured at the NDKSvelte level):
1. Fetches the user's NIP-60 wallet event (kind 17375)
2. Instantiates an `NDKCashuWallet` from the wallet event
3. Starts wallet monitoring
4. Sets it on `ndk.$wallet` so you can immediately access `ndk.$wallet.balance`

```svelte
<script lang="ts">
import { ndk } from '$lib/ndk';

// Login - wallet loads automatically
await ndk.$sessions.login(signer);

// Wallet is now ready to use
const balance = $derived(ndk.$wallet.balance);
</script>

<p>Balance: {balance} sats</p>

When you switch sessions or logout, the wallet updates automatically based on the active session's wallet.

Web of Trust

Powerful WoT filtering and ranking:

<script lang="ts">
  import { ndk } from "$lib/ndk";
  import { onMount } from "svelte";

  onMount(async () => {
    if (ndk.$sessions.current) {
      // Load WoT data
      await ndk.$wot.load({ maxDepth: 2 });

      // Enable automatic filtering on all subscriptions
      ndk.$wot.enableAutoFilter({
        maxDepth: 2,
        minScore: 0.5,
        includeUnknown: false,
      });
    }
  });

  // Subscriptions automatically filter by WoT when enabled
  const notes = ndk.$subscribe({ kinds: [1], limit: 100 });
</script>

WoT API

// Load WoT data
await ndk.$wot.load({ maxDepth?: number, maxFollows?: number, timeout?: number })

// Enable/disable automatic filtering
ndk.$wot.enableAutoFilter(options?)
ndk.$wot.disableAutoFilter()

// Query WoT
ndk.$wot.getScore(pubkey)              // number (0-1)
ndk.$wot.getDistance(pubkey)           // number | null
ndk.$wot.includes(pubkey, options?)    // boolean
ndk.$wot.shouldFilterEvent(event)      // boolean
ndk.$wot.rankEvents(events, options?)  // T[]

// State
ndk.$wot.loaded                        // boolean
ndk.$wot.autoFilterEnabled             // boolean

Mute Management

Mute management is handled directly by NDK core:

<script lang="ts">
  import { ndk } from "$lib/ndk";

  // Mute lists are automatically loaded when user logs in

  // Check if muted
  const isMuted = ndk.mutedIds.has(pubkey);
  const isWordMuted = ndk.muteFilter(event);

  // Mute/unmute
  ndk.mutedIds.set(pubkey, "p");
  ndk.mutedIds.delete(pubkey);

  ndk.mutedWords.add("spam");
  ndk.mutedWords.delete("spam");
</script>

Performance Note: Word filtering only runs on content kinds (1, 30023, 4, 1059, 30009, 1311, and kinds 1000-9999). Non-content events are not checked for muted words for performance.

Wallet Integration

Seamless integration with ndk-wallet:

<script lang="ts">
  import { ndk } from "$lib/ndk";
  import { NDKCashuWallet } from "@nostr-dev-kit/ndk-wallet";

  // Create and set wallet
  const cashuWallet = new NDKCashuWallet(ndk);
  await cashuWallet.init();
  ndk.$wallet.set(cashuWallet);

  // Reactive wallet state
  const balance = $derived(ndk.$wallet.balance);
  const connected = $derived(!!ndk.$wallet.wallet);
</script>

<p>Balance: {balance} sats</p>

Wallet API

// Save wallet configuration (creates or updates)
// Publishes both the wallet config (kind 17375) and mint list (kind 10019) for nutzap reception
await ndk.$wallet.save({
  mints: ['https://mint.example.com'],
  relays: ['wss://relay.example.com'],
})

// Set/clear wallet
ndk.$wallet.set(wallet)
ndk.$wallet.clear()
await ndk.$wallet.refreshBalance()

// State
ndk.$wallet.balance // number
ndk.$wallet.mints // string[] - configured mint URLs
ndk.$wallet.mintBalances // Mint[] - mints with balances (including 0 balance)
ndk.$wallet.relays // string[]

Relay Pool Monitoring

Monitor relay connections:

<script lang="ts">
  import { ndk } from "$lib/ndk";

  const connected = $derived(ndk.pool.connectedCount);
  const connecting = $derived(ndk.pool.connectingCount);
  const relays = $derived(ndk.pool.getConnectedRelays());
</script>

<p>Connected: {connected} | Connecting: {connecting}</p>

<ul>
  {#each relays as relay}
    <li>{relay.url}</li>
  {/each}
</ul>

Pool API

// Query relays
ndk.pool.getRelay(url) // RelayInfo | undefined
ndk.pool.getConnectedRelays() // RelayInfo[]

// State
ndk.pool.relays // Map<string, RelayInfo>
ndk.pool.connectedCount // number
ndk.pool.connectingCount // number

Advanced Patterns

Derived Subscriptions

Create derived reactive state from subscriptions:

<script lang="ts">
  const notes = ndk.$subscribe({ kinds: [1], authors: [pubkey] });

  // Derived state using $derived
  const recentNotes = $derived(notes.events.slice(0, 10));

  const notesByDay = $derived(
    notes.events.reduce(
      (acc, note) => {
        const day = new Date(note.created_at! * 1000).toDateString();
        (acc[day] ??= []).push(note);
        return acc;
      },
      {} as Record<string, NDKEvent[]>,
    ),
  );
</script>

Effect Hooks

Run side effects when subscription state changes:

<script lang="ts">
  const notes = ndk.$subscribe({ kinds: [1] });

  // Run effect when new events arrive
  $effect(() => {
    if (notes.events.length > 0) {
      console.log("New events:", notes.events.length);
      playNotificationSound();
    }
  });

  // Run once when EOSE is reached
  $effect(() => {
    if (notes.eosed) {
      console.log("Initial load complete");
    }
  });
</script>

Proper Use of EOSE

The eosed flag is for performance optimization and analytics, not loading states:

<script lang="ts">
  const notes = ndk.$subscribe({ kinds: [1] });

  // ✅ Good: Trigger pagination after initial load
  $effect(() => {
    if (notes.eosed && notes.count < 10) {
      notes.fetchMore(20);
    }
  });

  // ✅ Good: Performance analytics
  $effect(() => {
    if (notes.eosed) {
      console.log(`Loaded ${notes.count} events`);
    }
  });

  // ❌ Bad: Blocking UI
  // {#if !notes.eosed}<Spinner />{/if}
</script>

<!-- Just render events as they stream in -->
{#each notes.events as note}
  <Note {note} />
{/each}

Performance

Buffered Updates

By default, events are buffered for 30ms to batch DOM updates:

<script lang="ts">
  // High-frequency updates (default)
  const sub1 = ndk.$subscribe(filters, {
    bufferMs: 30, // Batch updates every 30ms
  });

  // Real-time updates (no buffering)
  const sub2 = ndk.$subscribe(filters, {
    bufferMs: false, // Update immediately
  });
</script>

Smart Deduplication

Events are automatically deduplicated using NDK's deduplication keys:

<script lang="ts">
  // Duplicate events are automatically filtered
  const sub = ndk.$subscribe([
    { kinds: [1], authors: [pubkey] },
    { kinds: [1], "#p": [pubkey] },
  ]);

  // Only unique events appear in sub.events
  // Replaceable events are automatically replaced with newer versions
</script>

Type Safety

Full TypeScript support with smart type inference:

import { ndk } from '$lib/ndk'
import { NDKHighlight } from '@nostr-dev-kit/ndk'

// Type is inferred as EventSubscription<NDKHighlight>
const highlights = ndk.$subscribe<NDKHighlight>({ kinds: [9802] }, { eventClass: NDKHighlight })

// highlights.events is NDKHighlight[]
highlights.events[0].highlightedContent // Type-safe

Migration from ndk-svelte

<!-- Old (ndk-svelte) -->
<script lang="ts">
import { onDestroy } from 'svelte';

const store = $ndk.storeSubscribe({ kinds: [1] });

onDestroy(() => {
  store.unsubscribe();
});
</script>

{#each $store as event}
  {event.content}
{/each}

<!-- New (svelte) -->
<script lang="ts">
const sub = ndk.$subscribe({ kinds: [1] });
// No manual cleanup needed
</script>

{#each sub.events as event}
  {event.content}
{/each}

Architecture

Class Hierarchy

NDKSvelte (extends NDK)
├── $subscribe() → Subscription<T>
├── $sessions → ReactiveSessionsStore
├── $wot → ReactiveWoTStore
├── $wallet → ReactiveWalletStore
├── $payments → ReactivePaymentsStore
└── $pool → ReactivePoolStore

Subscription<T>
├── events: T[] (reactive)
├── eosed: boolean (reactive)
├── count: number (derived)
├── isEmpty: boolean (derived)
├── start(), stop(), restart()
└── changeFilters(), clear()

Reactive Fetching

Fetch Multiple Events

<script lang="ts">
  import { ndk } from "$lib/ndk";

  const pubkey = $state("hex...");
  const notes = ndk.$fetchEvents(() => ({
    kinds: [1],
    authors: [pubkey],
    limit: 20,
  }));

  // Multiple filters
  const events = ndk.$fetchEvents(() => [
    { kinds: [1], authors: [pubkey1], limit: 10 },
    { kinds: [1], authors: [pubkey2], limit: 10 },
  ]);
</script>

{#each notes as note}
  <article>{note.content}</article>
{/each}

Examples

See the examples directory for complete working examples:

Coming Soon

  • Multi-user App - Account switching and management
  • Real-time Chat - Messaging with DMs
  • Advanced Patterns - Complex reactive patterns

API Reference

NDKSvelte

class NDKSvelte extends NDK {
  // Reactive stores ($ prefix indicates reactive state)
  $sessions: ReactiveSessionsStore
  $wot: ReactiveWoTStore
  $wallet: ReactiveWalletStore
  $pool: ReactivePoolStore

  // Reactive subscription
  $subscribe<T extends NDKEvent>(config: () => SubscribeConfig | undefined): Subscription<T>

  // Reactive fetching
  $fetchEvents(filters: () => NDKFilter | NDKFilter[] | undefined): NDKEvent[]
}

Subscription

class Subscription<T extends NDKEvent> {
  // Reactive $state properties
  events: T[]
  eosed: boolean
  error?: Error
  status: ConnectionStatus
  refCount: number

  // Derived getters
  count: number
  isEmpty: boolean

  // Filter property
  filters: NDKFilter[]

  // Methods
  start(): void
  stop(): void
  restart(): void
  clear(): void
  changeFilters(filters: NDKFilter[]): void
  fetchMore(limit: number): Promise<void>
  add(event: T): void
  remove(eventId: string): void
  ref(): number
  unref(): number
}

SubscriptionOptions

interface SubscriptionOptions {
  bufferMs?: number | false
  skipDeleted?: boolean
  eventClass?: typeof NDKEvent
  relaySet?: NDKRelaySet
  autoStart?: boolean
  onEvent?: (event: NDKEvent, relay?: NDKRelay) => void
  onEose?: () => void
}

Troubleshooting

Reactivity Not Working?

Make sure you're accessing properties in Svelte templates or reactive contexts:

<script lang="ts">
const sub = ndk.$subscribe({ kinds: [1] });

// ✅ Good - accessed in template, automatically reactive
</script>

{#each sub.events as event}
  <div>{event.content}</div>
{/each}

<!-- ✅ Good - accessed in effect -->
<script lang="ts">
$effect(() => {
  console.log('Events updated:', sub.events.length);
});
</script>

Key Points About Svelte 5 Reactivity

  • $state variables are reactive, but you must access them in reactive contexts for Svelte to track changes
  • Simply reading a value in regular JavaScript doesn't create a reactive dependency
  • Always access subscription properties in templates, $effect, or $derived to ensure reactivity
  • Arrays are mutated in place - The subscription internally uses .length = 0 and .push() to mutate the events array rather than replacing it

Philosophy & Design Decisions

Why Runes?

Svelte 5's runes provide fine-grained reactivity that's perfect for real-time data. Instead of stores everywhere, we use reactive classes that feel natural in Svelte 5.

Why Not Backwards Compatible?

Breaking free from legacy patterns lets us build something truly modern. svelte is designed for new projects and future-looking apps.

Why Namespaced Stores?

Namespacing stores under the NDK instance prevents global pollution and makes the API clearer. Everything related to NDK is accessible through a single import.

Why Beautiful APIs?

Code is read more than written. Beautiful, intuitive APIs make building Nostr apps a joy, not a chore.

Contributing

We're building the best Nostr library for Svelte. Join us!

License

MIT

Credits

Built with ❤️ by the Nostr Dev Kit team.