Regex public feeds in nostr
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)\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.
Did you enjoy this article?
Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.