| name | applesauce-core |
| description | 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. |
applesauce-core Skill
This skill provides comprehensive knowledge and patterns for working with applesauce-core, a library that provides reactive utilities and patterns for building Nostr clients.
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)
import {
getArticleTitle,
getArticleSummary,
getArticleImage,
getArticlePublished,
isValidArticle
} from 'applesauce-core/helpers';
// 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)
import {
getHighlightText,
getHighlightSourceUrl,
getHighlightSourceEventPointer,
getHighlightSourceAddressPointer,
getHighlightContext,
getHighlightComment,
getHighlightAttributions
} from 'applesauce-core/helpers';
// 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)
import { getNip10References } from 'applesauce-core/helpers';
// 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]
);
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-signers - Event signing abstractions
- svelte - Building reactive UIs
- nostr - Nostr protocol fundamentals