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.

$ Installieren

git clone https://github.com/purrgrammer/grimoire /tmp/grimoire && cp -r /tmp/grimoire/.claude/skills/applesauce-core ~/.claude/skills/grimoire

// tip: Run this command in your terminal to install the skill


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 (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]
);

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

  1. Single store instance - Use one EventStore per app
  2. Clear stale data - Implement cache limits
  3. Handle replaceable events - Let store manage deduplication
  4. Unsubscribe - Clean up subscriptions on component destroy

Query Optimization

  1. Use specific filters - Narrow queries perform better
  2. Limit results - Use limit for initial loads
  3. Cache queries - Reuse query instances
  4. Debounce updates - Throttle rapid changes

Memory Management

  1. Limit store size - Implement LRU or time-based eviction
  2. Clean up observables - Unsubscribe when done
  3. Use weak references - For profile caches
  4. Paginate large feeds - Don't load everything at once

Reactive Patterns

  1. Prefer observables - Over imperative queries
  2. Use operators - Transform data with RxJS
  3. Combine streams - For complex views
  4. 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

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