This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Build
cargo build
# Run (reads .env automatically via dotenvy)
cargo run
# Check for errors without building
cargo check
# Run clippy lints
cargo clippy
# Run a specific binary
cargo run --bin test_negentropy
# Format code
cargo fmt
# Run tests
cargo test
# Run a single test
cargo test <test_name>There are no automated tests beyond cargo test. The src/bin/test_negentropy.rs binary is a manual integration test for the negentropy sync protocol.
This is a Rust/Tokio async service that ingests Nostr events from multiple relays, stores them in PostgreSQL, and serves them via a REST API. All modules are declared in src/main.rs.
On startup, main.rs spins up several concurrent subsystems, all sharing Arc-wrapped state:
-
RelayIngester (
src/relay/ingester.rs) — connects to each relay URL as an independent tokio task, subscribes to live events via WebSocket, and routes inbound events throughEventRepository::insert_event. Uses a boundedmpscchannel to funnel events through a single processing worker (backpressure). -
MetadataResolver (
src/relay/metadata.rs) — receives pubkey hints over anmpscchannel from the ingester and fetches kind-0 metadata for new pubkeys. -
HybridCrawler (
src/crawler/orchestrator.rs) — historical backfill engine. Combines:- Negentropy set-reconciliation sync (bulk diff against relays)
- NIP-65 relay list routing (fetch each author from their own write relays)
- Legacy per-author time-range fetch fallback
-
REST API (
src/api/) — axum server onLISTEN_ADDR(default:8000). Rate-limited at 120 req/min per IP. IP whitelist viaRATELIMIT_WHITELIST. -
WebSocket search relay (
src/ws/mod.rs) — NIP-50 compatible endpoint onWS_LISTEN_ADDR(default:8001). Also serves feed endpoints for trending notes, followers, and ranked profile notes. -
Indexer relay (
src/indexer/mod.rs) — restricted read-only WebSocket relay on:8003(default). Serves only kinds 0, 3, 10002. Requiresauthorsfilter. Enabled viaENABLE_INDEXER=true. -
Scheduler relay (
src/scheduler/mod.rs) — accepts future-dated events and publishes them atcreated_attime on:8002(default). Enabled viaENABLE_SCHEDULER=true. -
Background tasks (inline in
main.rs):profile_searchmaterialized view refresh every 5 minutes- Analytics materialized views refresh every 30 minutes
- Daily analytics computation at midnight UTC (backfills last 30 days on startup)
AppState (defined in src/api/mod.rs) wraps:
EventRepository— all DB access; holdsPgPool,FollowerCache, andWotCacheStatsCache— Redis-backed counters and JSON response cachingCrawlQueue— crawler work queue (optional,Nonewhen crawler disabled)RelayFetcher— on-demand fetcher for missing events/profilesProfileSearchCache— in-memory cache ofprofile_searchMV for zero-DB-hit searches
insert_event applies kind-based routing:
- Kind 0 (metadata): stored only if author passes WoT check OR already has events
- Kind 1 (note): WoT-gated; stores event, inserts
event_refs, incrementsreply_counton target - Kind 3 (contact list): always processed; upserts social graph rows only (not stored as an event)
- Kind 6/16 (repost): counter-only; increments
repost_count, event not stored - Kind 7 (reaction): counter-only; increments
reaction_count, event not stored - Kind 9735 (zap): always stored; increments zap counters and parses bolt11 for sat amount
- Kind 10002 (relay list): always processed; upsert only
WotCache (src/wot_cache.rs) implements a two-level follower quality check: a pubkey passes if it is followed by at least WOT_THRESHOLD (default 21) pubkeys that themselves have at least MIN_FOLLOWER_THRESHOLD (default 5) followers. Cache refreshes every WOT_REFRESH_SECS (default 15 min). This gates kind-0 and kind-1 ingestion to filter low-quality content.
The HybridCrawler (src/crawler/orchestrator.rs) coordinates:
NegentropySyncer(src/crawler/negentropy.rs) — set-reconciliation against relay event setsRelayRouter(src/crawler/relay_router.rs) — looks up per-author NIP-65 write relays from therelay_liststableCrawlQueue(src/crawler/queue.rs) — priority queue in PostgreSQL usingFOR UPDATE SKIP LOCKEDfor safe concurrency; authors are tiered by follower count
Migrations in migrations/ run automatically via sqlx on startup (db::init_pool). Key tables:
events— one row per stored event;tagsJSONB with GIN index;content_tsvgenerated column for FTSevent_refs— directional edges (reply/reaction/repost/zap/mention/root)event_tags— normalized tag rows for fast lookupsfollows/follow_lists— social graph from kind-3 eventscrawl_state— per-author crawler progress (last fetched timestamp, tier)relay_lists— per-author NIP-65 relay URLsprofile_search— materialized view with profile metadata + follower counts + engagement scoresdaily_analytics/ analytics materialized views — aggregated daily stats
axum 0.8withwsfeature — HTTP + WebSocket serversqlx 0.8— async PostgreSQL (compile-time checked queries)tokio-tungstenite— outbound WebSocket relay connectionsnegentropy 0.5— set reconciliation protocol cratesecp256k1— event signature verificationbech32+hex— NIP-19 entity decoding (src/nip19.rs)
Copy .env.example to .env before first run. The most impactful non-default settings:
ENABLE_CRAWLER=true— enables historical backfill (defaulttruein code,falsein example)NEGENTROPY_ENABLED=true— uses set-reconciliation for bulk syncCRAWLER_USE_RELAY_LISTS=true— routes crawl requests to each author's own write relaysWOT_THRESHOLD— lower values ingest more content; higher values are more selectiveONDEMAND_FETCH_ENABLED=true— fetches missing events from relays on API miss