Applesauce v7 is out. This release rebuilds how the SDK talks to relays, so apps keep working when relays are slow, offline, or ask for authentication, and so loading large amounts of data stays fast.
This post was drafted by Claude, an AI, from the release notes and examples.
Reworked applesauce-relay#
Relay methods now come in two layers. The simple ones (req, event, negentropy, auth) send one message and return the answer, with no extra behavior. The high-level ones (request, subscription, publish, count, sync) handle authentication, retries, reconnects, and timeouts for you. Most apps should only need the high-level layer.
Requests to several relays no longer stop after a fixed five seconds. They wait until the relays go quiet. If every relay fails, the request now fails with every relay's error attached, so you can tell an empty result from a broken one.
Authentication is set on each request, and one connection can sign in as several users at once:
pool.subscription(relays, { kinds: [1059], "#p": pubkeys }, { waitForAuth: pubkeys });
await relay.publish(event, {
onAuthRequired: ({ relay }) => relay.authenticate(signer),
});
Examples:
Relay liveness#
The relay pool can now track which relays are healthy. It skips relays that keep failing, and when one comes back it adds it back into subscriptions that are already open. The loaders report their failures to it too, so a dead relay only costs you once.
const liveness = new RelayLiveness();
const pool = new RelayPool({ liveness });
Instead of testing every relay yourself, you can also trust NIP-66 relay monitors to tell you which relays are offline.
Examples:
A better timeline loader#
The timeline loader got a lot of fixes in v7. In v6, long scrolling sessions could quietly break: events were skipped at page edges, relays with different limits left gaps, a relay could be ignored for good after a cancelled request, newer events could stop loading, and one failing relay could stop the whole load. All of these are fixed, so a timeline can now scroll back through years of notes without missing anything.
To make those fixes stick, the loader no longer has a "load the next page" mode. You give it the window of time that is on screen, either once or as a stream that changes as the user scrolls, and it loads that window from every relay. Moving the window loads more, backward or forward:
const window$ = new BehaviorSubject({ since: now - 3600, until: now });
loader(window$).subscribe();
window$.next({ since: now - 7200, until: now }); // load older
Each relay keeps its own place and loads at its own pace, so the fastest relay can fill the screen while slower ones catch up. The loader keeps a buffer of events past each edge so scrolling feels instant, retries failing relays with backoff, and reports progress for every relay.
Two examples show everything the timeline loader can do:
- Infinite scroll scrolls through a user's notes from their outbox relays, with no jumps or gaps.
- Timeline scrubber lets you drag through a user's whole history, backward and forward, and shows each relay loading in real time.
Sync and loaders built for large data#
The NIP-77 sync loader now does most of the heavy lifting. It handles relay authentication, skips unhealthy relays, retries missing events, and reports progress for each relay. It also replaces the old user lists loader.
const sync = createSyncLoader({ eventStore, pool });
const run = sync({ relays, filter: { authors: [pubkey], kinds: [3, 10000] } });
run.status$.subscribe(); // starts syncing into the event store
The social graph loader crawls faster and reports its progress.
The loaders that fetch single events, profiles, and replaceable events are sturdier too. They send batches after 200ms instead of a full second, so profiles and quoted notes show up sooner. They limit how many requests are open at once and share identical requests. A slow or failing relay no longer holds up the fallback relays.
Examples:
A faster, more accurate outbox model#
The outbox model picks which relays to read from for each of your contacts, based on their NIP-65 relay lists. In v7 it is faster and much more accurate:
- Choosing relays for large contact lists is faster, and it updates incrementally instead of starting over.
- When contacts change, it no longer reloads every contact's relay list.
- The "relays per user" limit is respected, so one user can't use up all your connections and leave others uncovered.
- Blocked relays stay blocked, even when they appear in a contact's relay list.
- When follows or relay lists change, outbox timelines keep what they have already loaded instead of reloading every author.
const outboxMap$ = contacts$.pipe(includeMailboxes(eventStore), filterOptimalRelays(20, 3), map(createOutboxMap));
pool.outboxSubscription(outboxMap$, { kinds: [1] });
Examples:
- Social feed combines the outbox model with the timeline loader and a live outbox subscription.
- Contacts sync combines the outbox model with the sync loader to download every event your contacts posted in a date range.
- Client startup starts a client from its local cache, picks your contacts' relays, then syncs their lists and profiles.
Counting things on events#
The new count service asks relays how many events match, using NIP-45, without downloading the events themselves. It comes with reaction and comment counts built in, and you can count anything else that a filter can describe:
const counts = createCountService(pool, { relays });
counts.event(note).reactions$.subscribe(showReactions);
counts.event(note).metric({ id: "quotes:v1", filters: (e) => [{ kinds: [1], "#q": [e.id] }] });
It shares and caches answers, spreads requests across relays, and backs off when a relay is busy, so a client can show counts for every note in a long feed. Each count comes from a single relay, so it is that relay's answer rather than a total for the whole network.
Example: Event counts shows reaction, comment, repost, and zap counts loading as you scroll a feed.
Faster everywhere#
v7 includes dozens of performance fixes across the packages:
- The event store only updates the models that care about a new event, and checks filters much faster.
- Relay messages go straight to the subscription waiting for them.
- SQLite storage has better indexes and reuses its prepared queries.
- Note content parsing skips text that hasn't changed.
- NIP-44 encryption keys are cached instead of being recomputed for every message.
Large feeds, big contact lists, and long-running apps should all feel lighter after upgrading.
Rumors, faster verification, and spam filtering#
Event stores, models, and helpers now work with unsigned events (rumors) as well as signed ones. That makes private messages and drafts first-class, and there is a new RumorStore for keeping them.
The new optimistic verifier checks fewer signatures from relays that have proven trustworthy, which saves a lot of CPU when loading large feeds:
const verifier = new OptimisticEventVerifier({ storage });
const eventStore = new EventStore({ verifyEvent: verifier.verifyEvent });
applesauce-extra also includes nspam, a spam classifier that runs on the device and can hide spam replies from a feed.
Examples:
More new examples#
v7 also adds support for many more NIPs, with new examples you can try:
- Git issues and pull requests (NIP-34) and a single repo's issues
- App handlers (NIP-89)
- Napplets (NIP-5D) and running napplets
- Nsites (NIP-5A)
- Reports and moderation (NIP-56) and reported media
- User status (NIP-38)
- Torrents (NIP-35)
- Relay-based communities (NIP-29)
- Trusted providers (NIP-85)
- Thread notifications and zap notifications
There is also new support for NIP-39 external identities, NIP-7D forum threads, NIP-C7 chat messages, and hashtags and image details on profiles. Thread loading now includes NIP-22 comments.
Breaking changes#
This is a major release, and most of the breaking changes are in applesauce-relay and applesauce-loaders:
- The simple relay methods no longer authenticate, retry, or reconnect. Use the high-level methods for that.
- Requests to several relays finish when the relays go quiet, and they fail if every relay fails.
- The pool's
ignoreOfflineoption is replaced by relay liveness. - Sync, negentropy, and count return per-relay results instead of the old shapes.
- The timeline loader takes time windows, the social graph loader also returns its progress, and the user lists loader is removed.
- Logging no longer uses the
debugpackage. Turn on applesauce logs withenableLoggerNamespaces(). - Getters that read malformed content return
undefinedinstead of throwing. - Event stores and models take the event type as an extra type parameter.
EventFactory.kind()is removed.nostrconnect://signers must return the connection secret.- There are smaller changes in core, content, and SQLite.
The full migration guide is a step-by-step checklist with before and after code and search patterns, so you can hand it straight to a coding agent: Migrating from v6 to v7
Upgrade#
Every applesauce-* package moves to v7 together, so upgrade them all at once:
pnpm add applesauce-core@latest applesauce-common@latest applesauce-relay@latest applesauce-loaders@latest
Then update any other packages you use, such as applesauce-signers, applesauce-wallet, or applesauce-react.

Comments
No comments yet. Start the conversation.