The 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.

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.