feat: accept kind:1 text notes and kind:3 follow lists (NIP-01/NIP-02)#245
Merged
tlongwell-block merged 2 commits intomainfrom Apr 7, 2026
Merged
feat: accept kind:1 text notes and kind:3 follow lists (NIP-01/NIP-02)#245tlongwell-block merged 2 commits intomainfrom
tlongwell-block merged 2 commits intomainfrom
Conversation
…/NIP-16) - Add KIND_TEXT_NOTE constant and is_replaceable() helper to sprout-core - Allow kind:1 (MessagesWrite) and kind:3 (UsersWrite) in ingest allowlist - Route all NIP-16 replaceable kinds through replace_addressable_event() - Rewrite replace_addressable_event() for atomicity, stale-write protection, and NULL channel_id safety (pg_advisory_xact_lock + IS NOT DISTINCT FROM) - Include global events (channel_id=NULL) in NIP-50 and /api/search results - Add NIPs 2 and 16 to NIP-11 supported_nips - Update UsersWrite scope comment for kind:3
…tant, doc comments - Add NIP 16 to supported_nips (was missing despite commit message claiming it) - Add KIND_CHANNEL_METADATA constant for kind:41 (was a bare literal in is_replaceable) - Add NIP-33 exclusion comment on is_replaceable() to prevent future confusion - Add advisory lock collision semantics comment (extra serialization, not incorrectness)
This was referenced Apr 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Open Sprout to standard Nostr social events — kind:1 (text notes) and kind:3 (follow/contact lists). These are the two most fundamental Nostr event types; without them, standard clients like Damus and Amethyst pointed at Sprout cannot post or read basic notes.
kind:1 gives Sprout a global "company Twitter" feed. kind:3 gives clients the follow graph to filter it. Together they let Sprout act as a superset of a standard Nostr relay: a decent relay that also has enterprise team collaboration features.
Also fixes three bugs in
replace_addressable_event()that affected existing kind:0 profile replacement.Changes
sprout-core/kind.rsKIND_TEXT_NOTE = 1,is_replaceable()helper, updateALL_KINDSsprout-relay/handlers/ingest.rsMessagesWrite) and kind:3 (UsersWrite); route all replaceables throughreplace_addressable_event(); forcechannel_id = Nonefor global-only kinds; generalize channel-scoped token guardsprout-db/lib.rsreplace_addressable_event()— atomic tx,pg_advisory_xact_lock, stale-write protection with same-second id tie-break,IS NOT DISTINCT FROMfor NULL safety, checked INSERT with rollbacksprout-relay/handlers/req.rs__global__sentinelsprout-relay/api/search.rs/api/searchincludes global events; nullablechannel_namesprout-relay/handlers/side_effects.rswas_insertedbefore dispatching discovery eventssprout-search/index.rs__global__sentinel for Typesense (see below)sprout-search/query.rs__global__back toNonein search hitssprout-auth/scope.rsUsersWritecomment for kind:3sprout-relay/nip11.rssupported_nipsdesktop/src-tauri/models.rsSearchHitInfochannel fields →Option<String>desktop/src/*.ts{,x}channel_id/channel_nameacross types + UI null-guardsBugs fixed in
replace_addressable_event()These affected existing kind:0 profile replacement:
insert_event()called outside the tx. Crash between = data loss. → Single transaction.created_atcomparison + same-second id tie-break (lowest wins, per NIP-16).channel_id = $3is false when NULL. Global events silently failed to replace. →IS NOT DISTINCT FROM.pg_advisory_xact_lockwith FNV-1a hash key.Typesense discovery
During live testing, Typesense 27.1's
channel_id:__missing__filter returned 0 results even for documents where the field was truly absent. Tested absent, null, and omitted — all return 0. Appears to be a Typesense bug or undocumented limitation.Fix: Use
__global__sentinel value instead. Collision-proof (channel_id is UUID-shaped everywhere else), mapped back toNonein search hits. Requiresjust reindex-searchafter deploy to backfill historical docs. Not a regression — pre-existing global events (kind:0 only) were already excluded from search by the old.filter(|h| h.channel_id.is_some()).Testing
/api/searchcargo clippyclean,cargo fmtclean,biome checkcleanDeploy notes
just reindex-searchafter deploy to backfill the__global__sentinel for historical global events in Typesense.Out of scope
supported_nips(deferred until more replaceable kinds are accepted)