Skip to content

Regex public feeds in nostr

Silicology
Jul 5, 20263 min read

he cleanest way to implement public regex feeds in Nostr is to treat them as saved queries, not as a new Nostr protocol feature.

There are two common approaches.

1. Client-side filtering (recommended)

A user subscribes to a normal Nostr feed, and the client filters events locally using a regex.

For example:

Feed: Rust Jobs

Regex:
(?i)\b(rust|leptos|tokio|wasm)\b

The relay sends all text notes, and the client only displays matching events.

Advantages:

  • No relay modifications.
  • Works on every relay.
  • Privacy (relay doesn't know your regex).
  • Compatible with existing Nostr.

Disadvantages:

  • Downloads many unnecessary events.
  • Doesn't scale for large feeds.

Rust example:

use regex::Regex;

let regex = Regex::new(r"(?i)\brust\b").unwrap();

if regex.is_match(&event.content) {
    println!("Matched!");
}

2. Relay-side filtering (better for public feeds)

Imagine a relay dedicated to public feeds.

Users create feeds like:

rust
ai
india
bitcoin

Each feed stores

Feed ID
Regex
Owner
Description

When someone subscribes:

REQ
[
   "subid",
   {
      "kinds":[1],
      "#feed":["rust"]
   }
]

The relay translates

feed "rust"



(?i)\b(rust|cargo|tokio|leptos)\b

and only streams matching events.

This greatly reduces bandwidth.


Representing feeds in Nostr

You could publish feeds as a custom event.

Example:

{
  "kind": 30078,
  "content": "",
  "tags": [
    ["d", "rust"],
    ["title", "Rust Developers"],
    ["regex", "(?i)\\b(rust|cargo|tokio|leptos)\\b"]
  ]
}

Clients discover these feed definitions and allow users to subscribe.


Relay implementation (Rust)

Using the regex crate:

use regex::Regex;

struct Feed {
    id: String,
    regex: Regex,
}

fn matches(feed: &Feed, content: &str) -> bool {
    feed.regex.is_match(content)
}

When an event arrives:

for feed in feeds {
    if feed.regex.is_match(&event.content) {
        send_to_subscribers(feed.id.clone(), event.clone());
    }
}

Making it efficient

Compiling regex for every event is slow.

Instead:

struct Feed {
    id: String,
    regex: Regex,
}

Compile once when the feed is created.

Then reuse:

feed.regex.is_match(&event.content)

Scaling to thousands of feeds

Checking every regex against every event is O(events × feeds), which becomes expensive.

A better pipeline is:

Incoming Event


Tokenize words


Candidate feed lookup


Run regex only on candidate feeds


Send to subscribers

For example:

Event:

"I love Rust and Tokio."

Tokens:

rust
tokio
love



Candidate feeds:

Rust
Async
Programming



Run regex on only those feeds.

An inverted index (keyword → feed IDs) drastically reduces the number of regex evaluations.


Even faster alternative

If most feeds are keyword-based rather than requiring full regex power, use the aho-corasick crate. It matches thousands of patterns simultaneously in a single pass and is significantly faster than evaluating many independent regexes. You can reserve regex evaluation only for feeds that genuinely need complex expressions.

Recommendation

For a public, community-driven feed system:

  • Store feed definitions as Nostr events (e.g., a custom parameterized replaceable kind).
  • Relays maintain an in-memory cache of compiled regexes.
  • Users subscribe to feed IDs rather than sending raw regexes.
  • Use an inverted keyword index or Aho-Corasick prefilter before regex matching to scale efficiently.
  • Keep client-side regex filtering as a fallback for relays that don't support server-side public feeds.

This design is bandwidth-efficient, scales much better than naive regex matching, and remains compatible with the existing Nostr event model while allowing anyone to publish and share reusable public feeds.

Did you enjoy this article?

Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.

Across the AtmosphereDiscussions