Skip to content

feat: Follow-gated push notifications and NIP-01 replaceable event fixes #103

Open
Maphikza wants to merge 3 commits intoHORNET-Storage:mainfrom
Maphikza:feature/move-push-routes-to-relay-service
Open

feat: Follow-gated push notifications and NIP-01 replaceable event fixes #103
Maphikza wants to merge 3 commits intoHORNET-Storage:mainfrom
Maphikza:feature/move-push-routes-to-relay-service

Conversation

@Maphikza
Copy link
Member

@Maphikza Maphikza commented Mar 9, 2026

Summary

This PR adds follow-gated push notifications with a lazy TTL-based cache, fixes replaceable event handling in kind 0 and kind 3 handlers, and includes several push notification improvements.

Push Notification Enhancements

  • Follow-gated notifications (services/push/service.go): Recipients can be filtered so they only receive notifications from authors they follow. Uses a lazy in-memory cache with TTL expiry and bounded eviction — no external dependencies, no polling loops. The cache warms proactively when kind 3 (contact list) events arrive via ProcessEvent, and falls back to a store query on cache miss.
  • Kind 3 notifications disabled — contact list events are too spammy on nostr, so they no longer trigger push notifications (kind 3 removed from shouldNotify).
  • Referenced event context (84731d2): Push notification payloads now include context about the referenced event (e.g., the original note content when someone reacts to your post).

NIP-01 Replaceable Event Fixes

  • Kind 0 (profile metadata) and Kind 3 (contact list) handlers now enforce proper replaceable event semantics per NIP-01: incoming events are rejected if the relay already stores a newer version (compared by CreatedAt timestamp). Previously, older events could silently overwrite newer ones.
  • Cache warming guard (warmFollowCacheFromEvent): Before warming the follow cache from an incoming kind 3 event, the push service checks the store to ensure it's not overwriting the cache with stale data.

Configuration

Three new fields under push_notifications.service in config.yaml:

push_notifications:
  service:
    follow_gated: false       # Set to true to enable follow-gating
    follow_cache_size: 500    # Max cached follow lists
    follow_cache_ttl: 5m      # How long a cached follow list is valid

⚠️  Production config update required: Add these three fields to your production config.yaml under push_notifications.service. If omitted, Viper
 defaults kick in (follow_gated: false, follow_cache_size: 500, follow_cache_ttl: 5m) — so nothing breaks, but it's cleaner to have them
explicit.

follow_gated defaults to false — this is intentional. While traffic is low, we want all notifications to reach users so the app feels active.
When traffic grows and notification spam becomes a concern, flip it to true and users will only receive notifications from authors in their
contact list. Users who haven't published a contact list to the relay will continue to receive all notifications (permissive fallback — the
burden of publishing the contact list falls on the app, not the relay).

Push Notification Safety (addressing review feedback)

Two conditions were raised:
1. Only on event publish — ✅ Already satisfied. ProcessEvent is only called from the WebSocket EVENT handler in event.go, which only fires on
new event submissions.
2. No duplicates — ✅ Already satisfied. The processedIDs map in the push service deduplicates by event ID with automatic TTL-based cleanup.

Key Commits

- 84731d2 — Referenced event context in push payloads
- 29917f2 — Disable kind 3 push notifications (too spammy)
- 25928b1 — Follow-gated push notifications with lazy TTL cache
- b3d98db — NIP-01 replaceable event timestamp checks for kind 0 and kind 3
- ec7a1a2 — Default follow_gated to false for early-stage growth

Maphikza added 3 commits March 9, 2026 15:29
Only send push notifications to users from authors they follow.
Includes proactive cache warming from incoming kind 3 events,
bounded cache with oldest-10% eviction, and configurable TTL.
Kind 1059 (DMs) and test notifications are exempt from gating.
Users without a local contact list receive all notifications.
… 3 handlers

Kind 0 (profile) and kind 3 (contact list) handlers were blindly deleting
existing events without checking timestamps, allowing older events to
overwrite newer ones. Now both handlers compare CreatedAt timestamps and
reject incoming events that are older than what's already stored.

Also hardens the follow cache warming in push service to check the store
for a newer kind 3 before warming, preventing stale contact lists from
polluting the cache.
With low traffic, all notifications should reach users to keep the app
feeling active. When traffic grows and spam becomes a concern, operators
can enable follow-gating by setting follow_gated: true in config.yaml.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant