- Add 'serve' subcommand for ephemeral RAM-based relay at /dev/shm with open ACL mode for testing and benchmarking - Fix e-tag and p-tag decoding to use ValueHex()/ValueBinary() methods instead of Value() which returns raw bytes for binary-optimized storage - Document all command-line tools in readme.adoc (relay-tester, benchmark, stresstest, blossomtest, aggregator, convert, FIND, policytest, etc.) - Switch Docker images from Alpine to Debian for proper libsecp256k1 Schnorr signature and ECDH support required by Nostr - Upgrade Docker Go version from 1.21 to 1.25 - Add ramdisk mode (--ramdisk) to benchmark script for eliminating disk I/O bottlenecks in performance measurements - Add docker-compose.ramdisk.yml for tmpfs-based benchmark volumes - Add test coverage for privileged policy with binary-encoded p-tags - Fix blossom test to expect 200 OK for anonymous uploads when auth is not required (RequireAuth=false with ACL mode 'none') - Update follows ACL to handle both binary and hex p-tag formats - Grant owner access to all users in serve mode via None ACL - Add benchmark reports from multi-relay comparison run - Update CLAUDE.md with binary tag handling documentation - Bump version to v0.30.2
17 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
ORLY is a high-performance Nostr relay written in Go, designed for personal relays, small communities, and business deployments. It emphasizes low latency, custom cryptography optimizations, and embedded database performance.
Key Technologies:
- Language: Go 1.25.3+
- Database: Badger v4 (embedded key-value store) or DGraph (distributed graph database)
- Cryptography: Custom p8k library using purego for secp256k1 operations (no CGO)
- Web UI: Svelte frontend embedded in the binary
- WebSocket: gorilla/websocket for Nostr protocol
- Performance: SIMD-accelerated SHA256 and hex encoding, query result caching with zstd compression
Build Commands
Basic Build
# Build relay binary only
go build -o orly
# Pure Go build (no CGO) - this is the standard approach
CGO_ENABLED=0 go build -o orly
Build with Web UI
# Recommended: Use the provided script
./scripts/update-embedded-web.sh
# Manual build
cd app/web
bun install
bun run build
cd ../../
go build -o orly
Development Mode (Web UI Hot Reload)
# Terminal 1: Start relay with dev proxy
export ORLY_WEB_DISABLE=true
export ORLY_WEB_DEV_PROXY_URL=http://localhost:5173
./orly &
# Terminal 2: Start dev server
cd app/web && bun run dev
Testing
Run All Tests
# Standard test run
./scripts/test.sh
# Or manually with purego setup
CGO_ENABLED=0 go test ./...
# Note: libsecp256k1.so is included in the repository root
# Set LD_LIBRARY_PATH to use it: export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}$(pwd)"
Run Specific Package Tests
# Test database package
cd pkg/database && go test -v ./...
# Test protocol package
cd pkg/protocol && go test -v ./...
# Test with specific test function
go test -v -run TestSaveEvent ./pkg/database
Relay Protocol Testing
# Test relay protocol compliance
go run cmd/relay-tester/main.go -url ws://localhost:3334
# List available tests
go run cmd/relay-tester/main.go -list
# Run specific test
go run cmd/relay-tester/main.go -url ws://localhost:3334 -test "Basic Event"
Benchmarking
# Run Go benchmarks in specific package
go test -bench=. -benchmem ./pkg/database
# Note: Crypto benchmarks are now in the external nostr library at:
# https://git.mleku.dev/mleku/nostr
# Run full relay benchmark suite
cd cmd/benchmark
go run main.go -data-dir /tmp/bench-db -events 10000 -workers 4
# Benchmark reports are saved to cmd/benchmark/reports/
# The benchmark tool tests event storage, queries, and subscription performance
Running the Relay
Basic Run
# Build and run
go build -o orly && ./orly
# With environment variables
export ORLY_LOG_LEVEL=debug
export ORLY_PORT=3334
./orly
Get Relay Identity
# Print relay identity secret and pubkey
./orly identity
Common Configuration
# TLS with Let's Encrypt
export ORLY_TLS_DOMAINS=relay.example.com
# Admin configuration
export ORLY_ADMINS=npub1...
# Follows ACL mode
export ORLY_ACL_MODE=follows
# Enable sprocket event processing
export ORLY_SPROCKET_ENABLED=true
# Enable policy system
export ORLY_POLICY_ENABLED=true
# Database backend selection (badger or dgraph)
export ORLY_DB_TYPE=badger
export ORLY_DGRAPH_URL=localhost:9080 # Only for dgraph backend
# Query cache configuration (improves REQ response times)
export ORLY_QUERY_CACHE_SIZE_MB=512 # Default: 512MB
export ORLY_QUERY_CACHE_MAX_AGE=5m # Cache expiry time
# Database cache tuning (for Badger backend)
export ORLY_DB_BLOCK_CACHE_MB=512 # Block cache size
export ORLY_DB_INDEX_CACHE_MB=256 # Index cache size
Code Architecture
Repository Structure
Root Entry Point:
main.go- Application entry point with signal handling, profiling setup, and database initializationapp/main.go- Core relay server initialization and lifecycle management
Core Packages:
app/ - HTTP/WebSocket server and handlers
server.go- Main Server struct and HTTP request routinghandle-*.go- Nostr protocol message handlers (EVENT, REQ, COUNT, CLOSE, AUTH, DELETE)handle-websocket.go- WebSocket connection lifecycle and frame handlinglistener.go- Network listener setupsprocket.go- External event processing script managerpublisher.go- Event broadcast to active subscriptionspayment_processor.go- NWC integration for subscription paymentsblossom.go- Blob storage service initializationweb.go- Embedded web UI serving and dev proxyconfig/- Environment variable configuration using go-simpler.org/env
pkg/database/ - Database abstraction layer with multiple backend support
interface.go- Database interface definition for pluggable backendsfactory.go- Database backend selection (Badger or DGraph)database.go- Badger implementation with cache tuning and query cachesave-event.go- Event storage with index updatesquery-events.go- Main query execution engine with filter normalizationquery-for-*.go- Specialized query builders for different filter patternsindexes/- Index key construction for efficient lookupsexport.go/import.go- Event export/import in JSONL formatsubscriptions.go- Active subscription trackingidentity.go- Relay identity key managementmigrations.go- Database schema migration runner
pkg/protocol/ - Nostr protocol implementation
ws/- WebSocket message framing and parsingauth/- NIP-42 authentication challenge/responsepublish/- Event publisher for broadcasting to subscriptionsrelayinfo/- NIP-11 relay information documentdirectory/- Distributed directory service (NIP-XX)nwc/- Nostr Wallet Connect clientblossom/- Blob storage protocol
pkg/encoders/ - Optimized Nostr data encoding/decoding
event/- Event JSON marshaling/unmarshaling with buffer poolingfilter/- Filter parsing and validationbech32encoding/- npub/nsec/note encodinghex/- SIMD-accelerated hex encoding using templexxx/xhextimestamp/,kind/,tag/- Specialized field encoders
Cryptographic operations (from git.mleku.dev/mleku/nostr library)
- Pure Go secp256k1 using purego (no CGO) to dynamically load libsecp256k1.so
- Schnorr signature operations (NIP-01)
- ECDH for encrypted DMs (NIP-04, NIP-44)
- Public key recovery from signatures
libsecp256k1.so- Included in repository root for runtime loading
- Key derivation and conversion utilities
- SIMD-accelerated SHA256 using minio/sha256-simd
- SIMD-accelerated hex encoding using templexxx/xhex
pkg/acl/ - Access control systems
acl.go- ACL registry and interfacefollows.go- Follows-based whitelist (admins + their follows can write)managed.go- NIP-86 managed relay with role-based permissionsnone.go- Open relay (no restrictions)
pkg/policy/ - Event filtering and validation policies
- Policy configuration loaded from
~/.config/ORLY/policy.json - Per-kind size limits, age restrictions, custom scripts
- See
docs/POLICY_USAGE_GUIDE.mdfor configuration examples
pkg/sync/ - Distributed synchronization
cluster_manager.go- Active replication between relay peersrelay_group_manager.go- Relay group configuration (NIP-XX)manager.go- Distributed directory consensus
pkg/spider/ - Event syncing from other relays
spider.go- Spider manager for "follows" mode- Fetches events from admin relays for followed pubkeys
pkg/utils/ - Shared utilities
atomic/- Extended atomic operationsinterrupt/- Signal handling and graceful shutdownapputil/- Application-level utilities
Web UI (app/web/):
- Svelte-based admin interface
- Embedded in binary via
go:embed - Features: event browser, sprocket management, user admin, settings
Command-line Tools (cmd/):
relay-tester/- Nostr protocol compliance testingbenchmark/- Multi-relay performance comparisonstresstest/- Load testing toolaggregator/- Event aggregation utilityconvert/- Data format conversionpolicytest/- Policy validation testing
Important Patterns
Pure Go with Purego:
- All builds use
CGO_ENABLED=0 - The p8k crypto library (from
git.mleku.dev/mleku/nostr) usesgithub.com/ebitengine/puregoto dynamically loadlibsecp256k1.soat runtime - This avoids CGO complexity while maintaining C library performance
libsecp256k1.sois included in the repository root- Library must be in
LD_LIBRARY_PATHor same directory as binary for runtime loading
Database Backend Selection:
- Supports multiple backends via
ORLY_DB_TYPEenvironment variable - Badger (default): Embedded key-value store with custom indexing, ideal for single-instance deployments
- DGraph: Distributed graph database for larger, multi-node deployments
- Backend selected via factory pattern in
pkg/database/factory.go - All backends implement the same
Databaseinterface defined inpkg/database/interface.go
Database Query Pattern:
- Filters are analyzed in
get-indexes-from-filter.goto determine optimal query strategy - Filters are normalized before cache lookup, ensuring identical queries with different field ordering hit the cache
- Different query builders (
query-for-kinds.go,query-for-authors.go, etc.) handle specific filter patterns - All queries return event serials (uint64) for efficient joining
- Query results cached with zstd level 9 compression (configurable size and TTL)
- Final events fetched via
fetch-events-by-serials.go
WebSocket Message Flow:
handle-websocket.goaccepts connection and spawns goroutine- Incoming frames parsed by
pkg/protocol/ws/ - Routed to handlers:
handle-event.go,handle-req.go,handle-count.go, etc. - Events stored via
database.SaveEvent() - Active subscriptions notified via
publishers.Publish()
Configuration System:
- Uses
go-simpler.org/envfor struct tags - All config in
app/config/config.gowithORLY_prefix - Supports XDG directories via
github.com/adrg/xdg - Default data directory:
~/.local/share/ORLY
Event Publishing:
pkg/protocol/publish/manages publisher registry- Each WebSocket connection registers its subscriptions
publishers.Publish(event)broadcasts to matching subscribers- Efficient filter matching without re-querying database
Embedded Assets:
- Web UI built to
app/web/dist/ - Embedded via
//go:embeddirective inapp/web.go - Served at root path
/with API at/api/*
Domain Boundaries & Encapsulation:
- Library packages (e.g.,
pkg/policy) should NOT export internal state variables - Use unexported fields (lowercase) for internal state to enforce encapsulation at compile time
- Provide public API methods (e.g.,
IsEnabled(),CheckPolicy()) instead of exposing internals - When JSON unmarshalling is needed for unexported fields, use a shadow struct with custom
UnmarshalJSON - External packages (e.g.,
app/) should ONLY use public API methods, never access internal fields - DO NOT change unexported fields to exported when fixing bugs - this breaks the domain boundary
Binary-Optimized Tag Storage (IMPORTANT):
- The nostr library (
git.mleku.dev/mleku/nostr/encoders/tag) uses binary optimization foreandptags - When events are unmarshaled from JSON, 64-character hex values in e/p tags are converted to 33-byte binary format (32 bytes hash + null terminator)
- DO NOT use
tag.Value()directly for e/p tags - it returns raw bytes which may be binary, not hex - ALWAYS use these methods instead:
tag.ValueHex()- Returns hex string regardless of storage format (handles both binary and hex)tag.ValueBinary()- Returns 32-byte binary if stored in binary format, nil otherwise
- Example pattern for comparing pubkeys:
// CORRECT: Use ValueHex() for hex decoding pt, err := hex.Dec(string(pTag.ValueHex())) // WRONG: Value() may return binary bytes, not hex pt, err := hex.Dec(string(pTag.Value())) // Will fail for binary-encoded tags! - This optimization saves memory and enables faster comparisons in the database layer
Development Workflow
Making Changes to Web UI
- Edit files in
app/web/src/ - For hot reload:
cd app/web && bun run dev(withORLY_WEB_DISABLE=trueandORLY_WEB_DEV_PROXY_URL=http://localhost:5173) - For production build:
./scripts/update-embedded-web.sh
Adding New Nostr Protocol Handlers
- Create
app/handle-<message-type>.go - Add case in
app/handle-message.gomessage router - Implement handler following existing patterns
- Add tests in
app/<handler>_test.go
Adding Database Indexes
- Define index in
pkg/database/indexes/ - Add migration in
pkg/database/migrations.go - Update
save-event.goto populate index - Add query builder in
pkg/database/query-for-<index>.go - Update
get-indexes-from-filter.goto use new index
Environment Variables for Development
# Verbose logging
export ORLY_LOG_LEVEL=trace
export ORLY_DB_LOG_LEVEL=debug
# Enable profiling
export ORLY_PPROF=cpu
export ORLY_PPROF_HTTP=true # Serves on :6060
# Health check endpoint
export ORLY_HEALTH_PORT=8080
Profiling
# CPU profiling
export ORLY_PPROF=cpu
./orly
# Profile written on shutdown
# HTTP pprof server
export ORLY_PPROF_HTTP=true
./orly
# Visit http://localhost:6060/debug/pprof/
# Memory profiling
export ORLY_PPROF=memory
export ORLY_PPROF_PATH=/tmp/profiles
Deployment
Automated Deployment
# Deploy with systemd service
./scripts/deploy.sh
This script:
- Installs Go 1.25.3 if needed
- Builds relay with embedded web UI
- Installs to
~/.local/bin/orly - Creates systemd service
- Sets capabilities for port 443 binding
systemd Service Management
# Start/stop/restart
sudo systemctl start orly
sudo systemctl stop orly
sudo systemctl restart orly
# Enable on boot
sudo systemctl enable orly
# View logs
sudo journalctl -u orly -f
Manual Deployment
# Build for production
./scripts/update-embedded-web.sh
# Or build all platforms
./scripts/build-all-platforms.sh
Key Dependencies
github.com/dgraph-io/badger/v4- Embedded databasegithub.com/gorilla/websocket- WebSocket servergithub.com/minio/sha256-simd- SIMD SHA256github.com/templexxx/xhex- SIMD hex encodinggithub.com/ebitengine/purego- CGO-free C library loadinggo-simpler.org/env- Environment variable configurationlol.mleku.dev- Custom logging library
Testing Guidelines
- Test files use
_test.gosuffix - Use
github.com/stretchr/testifyfor assertions - Database tests require temporary database setup (see
pkg/database/testmain_test.go) - WebSocket tests should use
relay-testerpackage - Always clean up resources in tests (database, connections, goroutines)
Performance Considerations
- Query Cache: 512MB query result cache (configurable via
ORLY_QUERY_CACHE_SIZE_MB) with zstd level 9 compression reduces database load for repeated queries - Filter Normalization: Filters are normalized before cache lookup, so identical queries with different field ordering produce cache hits
- Database Caching: Tune
ORLY_DB_BLOCK_CACHE_MBandORLY_DB_INDEX_CACHE_MBfor workload (Badger backend only) - Query Optimization: Add indexes for common filter patterns; multiple specialized query builders optimize different filter combinations
- Batch Operations: ID lookups and event fetching use batch operations via
GetSerialsByIdsandFetchEventsBySerials - Memory Pooling: Use buffer pools in encoders (see
pkg/encoders/event/) - SIMD Operations: Leverage minio/sha256-simd and templexxx/xhex for cryptographic operations
- Goroutine Management: Each WebSocket connection runs in its own goroutine
Recent Optimizations
ORLY has received several significant performance improvements in recent updates:
Query Cache System (Latest)
- 512MB query result cache with zstd level 9 compression
- Filter normalization ensures cache hits regardless of filter field ordering
- Configurable size (
ORLY_QUERY_CACHE_SIZE_MB) and TTL (ORLY_QUERY_CACHE_MAX_AGE) - Dramatically reduces database load for repeated queries (common in Nostr clients)
- Cache key includes normalized filter representation for optimal hit rate
Badger Cache Tuning
- Optimized block cache (default 512MB, tune via
ORLY_DB_BLOCK_CACHE_MB) - Optimized index cache (default 256MB, tune via
ORLY_DB_INDEX_CACHE_MB) - Resulted in 10-15% improvement in most benchmark scenarios
- See git history for cache tuning evolution
Query Execution Improvements
- Multiple specialized query builders for different filter patterns:
query-for-kinds.go- Kind-based queriesquery-for-authors.go- Author-based queriesquery-for-tags.go- Tag-based queries- Combination builders for
kinds+authors,kinds+tags,kinds+authors+tags
- Batch operations for ID lookups via
GetSerialsByIds - Serial-based event fetching for efficiency
- Filter analysis in
get-indexes-from-filter.goselects optimal strategy
Release Process
- Update version in
pkg/version/versionfile (e.g., v1.2.3) - Create and push tag:
git tag v1.2.3 git push origin v1.2.3 - GitHub Actions workflow builds binaries for multiple platforms
- Release created automatically with binaries and checksums