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.
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)\bThe 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
bitcoinEach feed stores
Feed ID
Regex
Owner
DescriptionWhen someone subscribes:
REQ
[
"subid",
{
"kinds":[1],
"#feed":["rust"]
}
]The relay translates
feed "rust"
↓
(?i)\b(rust|cargo|tokio|leptos)\band 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 subscribersFor 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.