
Security News
Ruby's Bundler 4.0.18 Extends Cooldown to bundle lock and bundle cache
The supply chain control that delays freshly published gems now covers lockfile generation and gem vendoring in Ruby projects.
@nostr-wot/data
Advanced tools
Pure-function Nostr data layer: profiles, notes, threads, engagement, NIP-65 outbox. Optional SWR cache + React hooks.
Pure-function Nostr data layer. Profiles, notes, threads, follows, follower lists, engagement (reactions / reposts / zaps), NIP-65 outbox-model relay discovery, and a streaming subscription coalescer. Optional SWR cache + React hooks ship as separate entrypoints.
| Import path | What's in it | Depends on |
|---|---|---|
@nostr-wot/data | Pure fetchers, parsers, outbox helper, getPool(), sharedCoalescer | nostr-tools (peer) |
@nostr-wot/data/cache | SWR cache wrapping the fetchers; observable primitive; localStorage persistence | + the above |
@nostr-wot/data/react | useProfile, useNote, useThread, <NostrDataProvider>, plus the shared session context (<NostrSessionProvider>, useSession, useKEKSigner, …) | + react (peer) |
Use only what you need.
npm i @nostr-wot/data nostr-tools
import {
fetchProfile,
fetchNote,
fetchNotesByAuthor,
fetchThread,
fetchFollows,
fetchEngagement,
fetchRelayList,
relaysForAuthor,
setDefaultRelays,
setProfileAggregators,
} from "@nostr-wot/data";
setDefaultRelays(["wss://relay.damus.io", "wss://nos.lol"]);
setProfileAggregators(["wss://purplepag.es"]);
const profile = await fetchProfile("hex-pubkey");
// → { displayName, name, picture, banner, about, nip05, lud16, fetchedAt }
const note = await fetchNote("hex-event-id");
const notes = await fetchNotesByAuthor("hex-pubkey", { limit: 50 });
const replies = await fetchThread("hex-event-id");
const follows = await fetchFollows("hex-pubkey");
// → { event, pubkeys, fetchedAt }
const engagement = await fetchEngagement(["id1", "id2"]);
// → Map<id, { reactionCount, repostCount, zapTotalSats }>
// Outbox: union of defaults + the author's NIP-65 write relays
const relays = await relaysForAuthor("hex-pubkey");
const relayList = await fetchRelayList("hex-pubkey");
// → { read: string[], write: string[], event } | null
streamProfile yields entry updates as relay events arrive (instead of resolving on EOSE).
import { streamProfile } from "@nostr-wot/data";
for await (const entry of streamProfile("hex-pubkey")) {
// each yield is an updated ProfileEntry — useful for showing partial results
}
sharedCoalescer merges concurrent reads from across your app — DM cache, profile cache, follower lists, etc. — into a single REQ per relay-set within a 50ms window. When the last consumer unsubscribes, the underlying subscription tears down.
import { sharedCoalescer } from "@nostr-wot/data";
// Live subscription
const stop = sharedCoalescer.enqueue({
filters: [{ kinds: [1], authors: ["hex"], limit: 50 }],
relays: ["wss://relay.damus.io"],
onEvent: (e) => { /* ... */ },
onEose: (relay) => { /* ... */ },
});
// One-shot
const events = await sharedCoalescer.querySync(
[{ kinds: [10002], authors: ["hex"], limit: 1 }],
{ relays, timeoutMs: 5000 },
);
import {
getProfile,
getNote,
getThread,
getFollows,
getRelayList,
fetchEngagementBatch,
configurePersistence,
createKeyedObservable,
} from "@nostr-wot/data/cache";
configurePersistence({ namespace: "myapp", ttlMs: 24 * 3600_000 });
const profile = await getProfile("hex-pubkey");
// Cold-loads from localStorage if cached, refreshes in the background
// from relays as newer kind-0 events arrive.
The cache exposes its own primitive — createKeyedObservable<K, V>() — used internally by every cached fetcher and by @nostr-wot/dm/cache. Useful if you're building your own SWR-style stream:
const obs = createKeyedObservable<string, MyValue>();
obs.set("key", { ... });
obs.subscribe("key", (v) => { /* re-render */ });
const slot = obs.get("key");
import {
NostrDataProvider,
useProfile,
useNote,
useThread,
useEngagement,
useFollows,
useRelayList,
} from "@nostr-wot/data/react";
<NostrDataProvider
relays={["wss://relay.damus.io", "wss://nos.lol"]}
profileAggregators={["wss://purplepag.es"]}
persistence={{ namespace: "myapp", ttlMs: 86400_000 }}
>
<App />
</NostrDataProvider>;
function ProfileCard({ pubkey }: { pubkey: string }) {
const profile = useProfile(pubkey); // SWR
if (!profile) return <Skeleton />;
return <h1>{profile.displayName ?? profile.name}</h1>;
}
<NostrDataProvider> configures defaults (relays, aggregators, persistence) on mount. If you're using @nostr-wot/wot or nostr-wot-sdk, prefer <NostrSdkProvider> instead — it wraps <NostrDataProvider> and adds opt-in WoT context.
@nostr-wot/data/react also hosts the shared session context — the single mount point for the active NostrSigner and the user's pubkey. It lives here (not in @nostr-wot/ui) so non-UI packages — DM hooks, blossom uploads, wallet/zap hooks — can read the signer from context without depending on the React UI package.
import {
NostrSessionProvider,
useSession,
useSigner,
usePubkey,
useLogin,
useLogout,
useKEKSigner,
} from "@nostr-wot/data/react";
<NostrSessionProvider
initialSigner={someSigner /* optional — e.g. constructed at boot */}
onChange={({ signer, pubkey }) => /* mirror to your state */}
onLogout={async () => { /* clear app caches */ }}
>
<App />
</NostrSessionProvider>;
| Hook | Returns |
|---|---|
useSession() | { signer, pubkey, isLoading, error, setSigner, logout } |
useSigner() | The active NostrSigner or null |
usePubkey() | The active hex pubkey or null |
useLogin() | (signer) => Promise<void> callback — set the active signer |
useLogout() | () => Promise<void> callback — drops the signer + runs onLogout |
useKEKSigner() | A narrow KEKSigner (NIP-44 encrypt + decrypt + pubkey) when the active signer supports NIP-44, else null |
useKEKSigner exists because some operations — DM cache-key derivation, wallet local-store encryption — need NIP-44 specifically and must skip signers that don't expose it (e.g. NIP-46 bunkers with restricted perms). The hook returns null instead of throwing, so consumers can branch on capability:
const kek = useKEKSigner();
if (!kek) return <p>Your signer doesn't support NIP-44 — encrypted-at-rest cache disabled.</p>;
const cacheKey = await getOrCreateCacheKey(kek.pubkey, kek);
@nostr-wot/ui mounts and consumes this same context for its login flows, and re-exports the hooks for convenience. If you're only using @nostr-wot/data (no UI), construct a signer yourself (from @nostr-wot/signers) and call useLogin()(signer) to attach it.
Every fetcher routes through the outbox helper by default. When you ask for an author's content, the SDK resolves their kind-10002 (NIP-65) relay list and queries the union of their declared write relays + your defaults. This is the only reliable way to find content that isn't pinned to popular aggregators.
Override globally:
setDefaultRelays(["wss://relay.damus.io", "wss://nos.lol"]);
setProfileAggregators(["wss://purplepag.es"]);
Or per-call (most fetchers accept an optional relays override):
await fetchProfile(pubkey, ["wss://my-private-relay"]);
@nostr-wot/data owns a single SimplePool instance, accessible via getPool(). If you have your own pool (e.g. for binary-frame coercion or NDK interop), wire it once at startup:
import { setPool } from "@nostr-wot/data";
const pool = new SimplePool({ websocketImplementation: MyWebSocket });
setPool(pool);
Other @nostr-wot/* packages reuse this same pool — DM subscriptions, blossom uploads, WoT lookups, all share connections.
MIT
FAQs
Pure-function Nostr data layer: profiles, notes, threads, engagement, NIP-65 outbox. Optional SWR cache + React hooks.
We found that @nostr-wot/data demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
The supply chain control that delays freshly published gems now covers lockfile generation and gem vendoring in Ruby projects.

Security News
During a UK cyber test, a Mythos 5 agent used sockpuppets, social engineering, and prompt injection to try to get a maintainer to merge malware.

Company News
Socket is now in the AWS Security Hub Extended plan. Adopt it through AWS, apply committed spend, and block malicious open source packages.