Claude-skill-registry applesauce-core
This skill should be used when working with applesauce-core library for Nostr client development, including event stores, queries, observables, and client utilities. Provides comprehensive knowledge of applesauce patterns for building reactive Nostr applications.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/applesauce-core" ~/.claude/skills/majiayu000-claude-skill-registry-applesauce-core && rm -rf "$T"
skills/data/applesauce-core/SKILL.md- references API keys
applesauce-core Skill (v5)
This skill provides comprehensive knowledge and patterns for working with applesauce-core v5, a library that provides reactive utilities and patterns for building Nostr clients.
Note: applesauce v5 introduced package reorganization:
- Protocol-level code stays in
applesauce-core - Social/NIP-specific helpers moved to
applesauce-common
moved fromEventFactory
toapplesauce-factoryapplesauce-core/event-factory
renamed toActionHub
inActionRunnerapplesauce-actions
When to Use This Skill
Use this skill when:
- Building reactive Nostr applications
- Managing event stores and caches
- Working with observable patterns for Nostr
- Implementing real-time updates
- Building timeline and feed views
- Managing replaceable events
- Working with profiles and metadata
- Creating efficient Nostr queries
Core Concepts
applesauce-core Overview
applesauce-core provides:
- Event stores - Reactive event caching and management
- Queries - Declarative event querying patterns
- Observables - RxJS-based reactive patterns
- Profile helpers - Profile metadata management
- Timeline utilities - Feed and timeline building
- NIP helpers - NIP-specific utilities
Installation
npm install applesauce-core
Basic Architecture
applesauce-core is built on reactive principles:
- Events are stored in reactive stores
- Queries return observables that update when new events arrive
- Components subscribe to observables for real-time updates
Event Store
Creating an Event Store
import { EventStore } from 'applesauce-core'; // Create event store const eventStore = new EventStore(); // Add events eventStore.add(event1); eventStore.add(event2); // Add multiple events eventStore.addMany([event1, event2, event3]); // Check if event exists const exists = eventStore.has(eventId); // Get event by ID const event = eventStore.get(eventId); // Remove event eventStore.remove(eventId); // Clear all events eventStore.clear();
Event Store Queries
// Get all events const allEvents = eventStore.getAll(); // Get events by filter const filtered = eventStore.filter({ kinds: [1], authors: [pubkey] }); // Get events by author const authorEvents = eventStore.getByAuthor(pubkey); // Get events by kind const textNotes = eventStore.getByKind(1);
Replaceable Events
applesauce-core handles replaceable events automatically:
// For kind 0 (profile), only latest is kept eventStore.add(profileEvent1); // stored eventStore.add(profileEvent2); // replaces if newer // For parameterized replaceable (30000-39999) eventStore.add(articleEvent); // keyed by author + kind + d-tag // Get replaceable event const profile = eventStore.getReplaceable(0, pubkey); const article = eventStore.getReplaceable(30023, pubkey, 'article-slug');
Queries
Query Patterns
import { createQuery } from 'applesauce-core'; // Create a query const query = createQuery(eventStore, { kinds: [1], limit: 50 }); // Subscribe to query results query.subscribe(events => { console.log('Current events:', events); }); // Query updates automatically when new events added eventStore.add(newEvent); // Subscribers notified
Timeline Query
import { TimelineQuery } from 'applesauce-core'; // Create timeline for user's notes const timeline = new TimelineQuery(eventStore, { kinds: [1], authors: [userPubkey] }); // Get observable of timeline const timeline$ = timeline.events$; // Subscribe timeline$.subscribe(events => { // Events sorted by created_at, newest first renderTimeline(events); });
Profile Query
import { ProfileQuery } from 'applesauce-core'; // Query profile metadata const profileQuery = new ProfileQuery(eventStore, pubkey); // Get observable const profile$ = profileQuery.profile$; profile$.subscribe(profile => { if (profile) { console.log('Name:', profile.name); console.log('Picture:', profile.picture); } });
Observables
Working with RxJS
applesauce-core uses RxJS observables:
import { map, filter, distinctUntilChanged } from 'rxjs/operators'; // Transform query results const names$ = profileQuery.profile$.pipe( filter(profile => profile !== null), map(profile => profile.name), distinctUntilChanged() ); // Combine multiple observables import { combineLatest } from 'rxjs'; const combined$ = combineLatest([ timeline$, profile$ ]).pipe( map(([events, profile]) => ({ events, authorName: profile?.name })) );
Creating Custom Observables
import { Observable } from 'rxjs'; function createEventObservable(store, filter) { return new Observable(subscriber => { // Initial emit subscriber.next(store.filter(filter)); // Subscribe to store changes const unsubscribe = store.onChange(() => { subscriber.next(store.filter(filter)); }); // Cleanup return () => unsubscribe(); }); }
Profile Helpers
Profile Metadata
import { parseProfile, ProfileContent } from 'applesauce-core'; // Parse kind 0 content const profileEvent = await getProfileEvent(pubkey); const profile = parseProfile(profileEvent); // Profile fields console.log(profile.name); // Display name console.log(profile.about); // Bio console.log(profile.picture); // Avatar URL console.log(profile.banner); // Banner image URL console.log(profile.nip05); // NIP-05 identifier console.log(profile.lud16); // Lightning address console.log(profile.website); // Website URL
Profile Store
import { ProfileStore } from 'applesauce-core'; const profileStore = new ProfileStore(eventStore); // Get profile observable const profile$ = profileStore.getProfile(pubkey); // Get multiple profiles const profiles$ = profileStore.getProfiles([pubkey1, pubkey2]); // Request profile load (triggers fetch if not cached) profileStore.requestProfile(pubkey);
Timeline Utilities
Building Feeds
import { Timeline } from 'applesauce-core'; // Create timeline const timeline = new Timeline(eventStore); // Add filter timeline.setFilter({ kinds: [1, 6], authors: followedPubkeys }); // Get events observable const events$ = timeline.events$; // Load more (pagination) timeline.loadMore(50); // Refresh (get latest) timeline.refresh();
Thread Building
import { ThreadBuilder } from 'applesauce-core'; // Build thread from root event const thread = new ThreadBuilder(eventStore, rootEventId); // Get thread observable const thread$ = thread.thread$; thread$.subscribe(threadData => { console.log('Root:', threadData.root); console.log('Replies:', threadData.replies); console.log('Reply count:', threadData.replyCount); });
Reactions and Zaps
import { ReactionStore, ZapStore } from 'applesauce-core'; // Reactions const reactionStore = new ReactionStore(eventStore); const reactions$ = reactionStore.getReactions(eventId); reactions$.subscribe(reactions => { console.log('Likes:', reactions.likes); console.log('Custom:', reactions.custom); }); // Zaps const zapStore = new ZapStore(eventStore); const zaps$ = zapStore.getZaps(eventId); zaps$.subscribe(zaps => { console.log('Total sats:', zaps.totalAmount); console.log('Zap count:', zaps.count); });
Helper Functions & Caching
Applesauce Helper System
applesauce-core provides 60+ helper functions for extracting data from Nostr events. Critical: These helpers cache their results internally using symbols, so you don't need
when calling them.useMemo
// ❌ WRONG - Unnecessary memoization const title = useMemo(() => getArticleTitle(event), [event]); const text = useMemo(() => getHighlightText(event), [event]); // ✅ CORRECT - Helpers cache internally const title = getArticleTitle(event); const text = getHighlightText(event);
How Helper Caching Works
import { getOrComputeCachedValue } from 'applesauce-core/helpers'; // Helpers use symbol-based caching const symbol = Symbol('ArticleTitle'); export function getArticleTitle(event) { return getOrComputeCachedValue(event, symbol, () => { // This expensive computation only runs once return getTagValue(event, 'title') || 'Untitled'; }); } // First call - computes and caches const title1 = getArticleTitle(event); // Computation happens // Second call - returns cached value const title2 = getArticleTitle(event); // Instant, from cache // Same reference console.log(title1 === title2); // true
Tag Helpers
import { getTagValue, hasNameValueTag } from 'applesauce-core/helpers'; // Get single tag value (searches hidden tags first) const dTag = getTagValue(event, 'd'); const title = getTagValue(event, 'title'); const url = getTagValue(event, 'r'); // Check if tag exists with value const hasTag = hasNameValueTag(event, 'client', 'grimoire');
Note: applesauce only provides
getTagValue (singular). For multiple values, implement your own:
function getTagValues(event, tagName) { return event.tags .filter(tag => tag[0] === tagName && tag[1]) .map(tag => tag[1]); }
Article Helpers (NIP-23)
Note: Article helpers moved to
applesauce-common in v5.
import { getArticleTitle, getArticleSummary, getArticleImage, getArticlePublished, isValidArticle } from 'applesauce-common/helpers/article'; // All cached automatically const title = getArticleTitle(event); const summary = getArticleSummary(event); const image = getArticleImage(event); const publishedAt = getArticlePublished(event); // Validation if (isValidArticle(event)) { console.log('Valid article event'); }
Highlight Helpers (NIP-84)
Note: Highlight helpers moved to
applesauce-common in v5.
import { getHighlightText, getHighlightSourceUrl, getHighlightSourceEventPointer, getHighlightSourceAddressPointer, getHighlightContext, getHighlightComment, getHighlightAttributions } from 'applesauce-common/helpers/highlight'; // All cached - no useMemo needed const text = getHighlightText(event); const url = getHighlightSourceUrl(event); const eventPointer = getHighlightSourceEventPointer(event); const addressPointer = getHighlightSourceAddressPointer(event); const context = getHighlightContext(event); const comment = getHighlightComment(event); const attributions = getHighlightAttributions(event);
Profile Helpers
import { getProfileContent, getDisplayName, getProfilePicture, isValidProfile } from 'applesauce-core/helpers'; // Parse profile JSON (cached) const profile = getProfileContent(profileEvent); // Get display name with fallback const name = getDisplayName(profile, 'Anonymous'); // Get profile picture with fallback const avatar = getProfilePicture(profile, '/default-avatar.png'); // Validation if (isValidProfile(event)) { const profile = getProfileContent(event); }
Pointer Helpers (NIP-19)
import { parseCoordinate, getEventPointerFromETag, getEventPointerFromQTag, getAddressPointerFromATag, getProfilePointerFromPTag, getEventPointerForEvent, getAddressPointerForEvent } from 'applesauce-core/helpers'; // Parse "a" tag coordinate (30023:pubkey:identifier) const aTag = event.tags.find(t => t[0] === 'a')?.[1]; const pointer = parseCoordinate(aTag); console.log(pointer.kind, pointer.pubkey, pointer.identifier); // Extract pointers from tags const eTag = event.tags.find(t => t[0] === 'e'); const eventPointer = getEventPointerFromETag(eTag); const qTag = event.tags.find(t => t[0] === 'q'); const quotePointer = getEventPointerFromQTag(qTag); // Create pointer from event const pointer = getEventPointerForEvent(event, ['wss://relay.example.com']); const address = getAddressPointerForEvent(replaceableEvent);
Reaction Helpers
import { getReactionEventPointer, getReactionAddressPointer } from 'applesauce-core/helpers'; // Get what the reaction is reacting to const eventPointer = getReactionEventPointer(reactionEvent); const addressPointer = getReactionAddressPointer(reactionEvent); if (eventPointer) { console.log('Reacted to event:', eventPointer.id); }
Threading Helpers (NIP-10)
Note: Threading helpers moved to
applesauce-common in v5.
import { getNip10References } from 'applesauce-common/helpers/threading'; // Parse NIP-10 thread structure (cached) const refs = getNip10References(event); if (refs.root) { console.log('Root event:', refs.root.e); console.log('Root address:', refs.root.a); } if (refs.reply) { console.log('Reply to event:', refs.reply.e); console.log('Reply to address:', refs.reply.a); }
Filter Helpers
import { isFilterEqual, matchFilter, matchFilters, mergeFilters } from 'applesauce-core/helpers'; // Compare filters (better than JSON.stringify) const areEqual = isFilterEqual(filter1, filter2); // Check if event matches filter const matches = matchFilter({ kinds: [1], authors: [pubkey] }, event); // Check against multiple filters const matchesAny = matchFilters([filter1, filter2], event); // Merge filters const combined = mergeFilters(filter1, filter2);
Available Helper Categories
applesauce-core provides helpers for:
- Tags: getTagValue, hasNameValueTag, tag type checks
- Articles: getArticleTitle, getArticleSummary, getArticleImage
- Highlights: 7+ helpers for highlight extraction
- Profiles: getProfileContent, getDisplayName, getProfilePicture
- Pointers: parseCoordinate, pointer extraction/creation
- Reactions: getReactionEventPointer, getReactionAddressPointer
- Threading: getNip10References for NIP-10 threads
- Comments: getCommentReplyPointer for NIP-22
- Filters: isFilterEqual, matchFilter, mergeFilters
- Bookmarks: bookmark list parsing
- Emoji: custom emoji extraction
- Zaps: zap parsing and validation
- Calendars: calendar event helpers
- Encryption: NIP-04, NIP-44 helpers
- And 40+ more - explore
node_modules/applesauce-core/dist/helpers/
When NOT to Use useMemo with Helpers
// ❌ DON'T memoize applesauce helper calls const title = useMemo(() => getArticleTitle(event), [event]); const summary = useMemo(() => getArticleSummary(event), [event]); // ✅ DO call helpers directly const title = getArticleTitle(event); const summary = getArticleSummary(event); // ❌ DON'T memoize helpers that wrap other helpers function getRepoName(event) { return getTagValue(event, 'name'); } const name = useMemo(() => getRepoName(event), [event]); // ✅ DO call directly (caching propagates) const name = getRepoName(event); // ✅ DO use useMemo for expensive transformations const sorted = useMemo( () => events.sort((a, b) => b.created_at - a.created_at), [events] ); // ✅ DO use useMemo for object creation const options = useMemo( () => ({ fallbackRelays, timeout: 1000 }), [fallbackRelays] );
EventFactory
Overview
The
EventFactory class (moved to applesauce-core/event-factory in v5) provides a unified API for creating and signing Nostr events using blueprints.
import { EventFactory } from 'applesauce-core/event-factory'; // Create factory const factory = new EventFactory(); // Set signer (required for signing) factory.setSigner(signer); // Optional context factory.setClient({ name: 'grimoire', address: { pubkey, identifier } });
Creating Events with Blueprints
Blueprints are templates for event creation. See the applesauce-common skill for detailed blueprint documentation.
import { NoteBlueprint, NoteReplyBlueprint } from 'applesauce-common/blueprints'; // Create a note const draft = await factory.create( NoteBlueprint, 'Hello #nostr!', { emojis: [{ shortcode: 'wave', url: 'https://...' }] } ); // Sign the draft const event = await factory.sign(draft); // Or combine: create and sign const event = await factory.sign( await factory.create(NoteBlueprint, content, options) );
Factory Methods
// Create from blueprint const draft = await factory.create(Blueprint, ...args); // Sign event template const event = await factory.sign(draft); // Stamp (add pubkey without signing) const unsigned = await factory.stamp(draft); // Build with operations const draft = await factory.build(template, ...operations); // Modify existing event const modified = await factory.modify(event, ...operations);
Using with Actions
EventFactory integrates seamlessly with the action system:
import accountManager from '@/services/accounts'; import { EventFactory } from 'applesauce-core/event-factory'; import { NoteBlueprint } from 'applesauce-common/blueprints'; const account = accountManager.active; const signer = account.signer; const factory = new EventFactory(); factory.setSigner(signer); // Create and sign const draft = await factory.create(NoteBlueprint, content, options); const event = await factory.sign(draft); // Publish await pool.publish(relays, event);
Common Patterns
Creating with custom tags:
// Let blueprint handle automatic extraction const draft = await factory.create(NoteBlueprint, content, { emojis }); // Add custom tags afterward draft.tags.push(['client', 'grimoire', '31990:...']); draft.tags.push(['imeta', `url ${blob.url}`, `x ${blob.sha256}`]); // Sign const event = await factory.sign(draft);
Error handling:
try { const draft = await factory.create(NoteBlueprint, content, options); const event = await factory.sign(draft); await pool.publish(relays, event); } catch (error) { if (error.message.includes('User rejected')) { // Handle rejection } else { // Handle other errors } }
Optimistic updates:
// Create and sign const event = await factory.sign( await factory.create(NoteBlueprint, content, options) ); // Add to local store immediately eventStore.add(event); // Publish in background pool.publish(relays, event).catch(err => { // Remove from store on failure eventStore.remove(event.id); });
NIP Helpers
NIP-05 Verification
import { verifyNip05 } from 'applesauce-core'; // Verify NIP-05 const result = await verifyNip05('alice@example.com', expectedPubkey); if (result.valid) { console.log('NIP-05 verified'); } else { console.log('Verification failed:', result.error); }
NIP-10 Reply Parsing
import { parseReplyTags } from 'applesauce-core'; // Parse reply structure const parsed = parseReplyTags(event); console.log('Root event:', parsed.root); console.log('Reply to:', parsed.reply); console.log('Mentions:', parsed.mentions);
NIP-65 Relay Lists
import { parseRelayList } from 'applesauce-core'; // Parse relay list event (kind 10002) const relays = parseRelayList(relayListEvent); console.log('Read relays:', relays.read); console.log('Write relays:', relays.write);
Integration with nostr-tools
Using with SimplePool
import { SimplePool } from 'nostr-tools'; import { EventStore } from 'applesauce-core'; const pool = new SimplePool(); const eventStore = new EventStore(); // Load events into store pool.subscribeMany(relays, [filter], { onevent(event) { eventStore.add(event); } }); // Query store reactively const timeline$ = createTimelineQuery(eventStore, filter);
Publishing Events
import { finalizeEvent } from 'nostr-tools'; // Create event const event = finalizeEvent({ kind: 1, content: 'Hello!', created_at: Math.floor(Date.now() / 1000), tags: [] }, secretKey); // Add to local store immediately (optimistic update) eventStore.add(event); // Publish to relays await pool.publish(relays, event);
Svelte Integration
Using in Svelte Components
<script> import { onMount, onDestroy } from 'svelte'; import { EventStore, TimelineQuery } from 'applesauce-core'; export let pubkey; const eventStore = new EventStore(); let events = []; let subscription; onMount(() => { const timeline = new TimelineQuery(eventStore, { kinds: [1], authors: [pubkey] }); subscription = timeline.events$.subscribe(e => { events = e; }); }); onDestroy(() => { subscription?.unsubscribe(); }); </script> {#each events as event} <div class="event"> {event.content} </div> {/each}
Svelte Store Adapter
import { readable } from 'svelte/store'; // Convert RxJS observable to Svelte store function fromObservable(observable, initialValue) { return readable(initialValue, set => { const subscription = observable.subscribe(set); return () => subscription.unsubscribe(); }); } // Usage const events$ = timeline.events$; const eventsStore = fromObservable(events$, []);
<script> import { eventsStore } from './stores.js'; </script> {#each $eventsStore as event} <div>{event.content}</div> {/each}
Best Practices
Store Management
- Single store instance - Use one EventStore per app
- Clear stale data - Implement cache limits
- Handle replaceable events - Let store manage deduplication
- Unsubscribe - Clean up subscriptions on component destroy
Query Optimization
- Use specific filters - Narrow queries perform better
- Limit results - Use limit for initial loads
- Cache queries - Reuse query instances
- Debounce updates - Throttle rapid changes
Memory Management
- Limit store size - Implement LRU or time-based eviction
- Clean up observables - Unsubscribe when done
- Use weak references - For profile caches
- Paginate large feeds - Don't load everything at once
Reactive Patterns
- Prefer observables - Over imperative queries
- Use operators - Transform data with RxJS
- Combine streams - For complex views
- Handle loading states - Show placeholders
Common Patterns
Event Deduplication
// EventStore handles deduplication automatically eventStore.add(event1); eventStore.add(event1); // No duplicate // For manual deduplication const seen = new Set(); events.filter(e => { if (seen.has(e.id)) return false; seen.add(e.id); return true; });
Optimistic Updates
async function publishNote(content) { // Create event const event = await createEvent(content); // Add to store immediately (optimistic) eventStore.add(event); try { // Publish to relays await pool.publish(relays, event); } catch (error) { // Remove on failure eventStore.remove(event.id); throw error; } }
Loading States
import { BehaviorSubject, combineLatest } from 'rxjs'; const loading$ = new BehaviorSubject(true); const events$ = timeline.events$; const state$ = combineLatest([loading$, events$]).pipe( map(([loading, events]) => ({ loading, events, empty: !loading && events.length === 0 })) ); // Start loading loading$.next(true); await loadEvents(); loading$.next(false);
Infinite Scroll
function createInfiniteScroll(timeline, pageSize = 50) { let loading = false; async function loadMore() { if (loading) return; loading = true; await timeline.loadMore(pageSize); loading = false; } function onScroll(event) { const { scrollTop, scrollHeight, clientHeight } = event.target; if (scrollHeight - scrollTop <= clientHeight * 1.5) { loadMore(); } } return { loadMore, onScroll }; }
Troubleshooting
Common Issues
Events not updating:
- Check subscription is active
- Verify events are being added to store
- Ensure filter matches events
Memory growing:
- Implement store size limits
- Clean up subscriptions
- Use weak references where appropriate
Slow queries:
- Add indexes for common queries
- Use more specific filters
- Implement pagination
Stale data:
- Implement refresh mechanisms
- Set up real-time subscriptions
- Handle replaceable event updates
References
- applesauce GitHub: https://github.com/hzrd149/applesauce
- RxJS Documentation: https://rxjs.dev
- nostr-tools: https://github.com/nbd-wtf/nostr-tools
- Nostr Protocol: https://github.com/nostr-protocol/nostr
Related Skills
- nostr-tools - Lower-level Nostr operations
- applesauce-common - Social/NIP-specific helpers (article, highlight, threading, zap, etc.)
- applesauce-signers - Event signing abstractions
- svelte - Building reactive UIs
- nostr - Nostr protocol fundamentals