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
-
EventFactory moved from applesauce-factory to applesauce-core/event-factory
-
ActionHub renamed to ActionRunner in applesauce-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 useMemo when calling them.
// ❌ 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