Introduced WasmDB as a new IndexedDB backend for WebAssembly environments, replicating Badger's schema for compatibility. Enhanced the query caching system with optional configuration to improve memory usage efficiency. Improved the policy system with new permissive overrides and clarified read vs write applicability for better flexibility.
38 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), Neo4j (social graph), or WasmDB (IndexedDB for WebAssembly)
- 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, optional query result caching with zstd compression
- Social Graph: Neo4j backend with Web of Trust (WoT) extensions for trust metrics
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
Get Version
# Print version and exit
./orly version
# Also accepts: -v, --v, -version, --version
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, neo4j, or wasmdb)
export ORLY_DB_TYPE=badger
# Neo4j configuration (only when ORLY_DB_TYPE=neo4j)
export ORLY_NEO4J_URI=bolt://localhost:7687
export ORLY_NEO4J_USER=neo4j
export ORLY_NEO4J_PASSWORD=password
# Query cache configuration (disabled by default to reduce memory usage)
export ORLY_QUERY_CACHE_DISABLED=false # Set to false to enable caching
export ORLY_QUERY_CACHE_SIZE_MB=512 # Cache size when enabled
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
export ORLY_DB_ZSTD_LEVEL=1 # ZSTD level: 0=off, 1=fast, 3=default, 9=best
# Serial cache for compact event storage (Badger backend)
export ORLY_SERIAL_CACHE_PUBKEYS=100000 # Max pubkeys to cache (~3.2MB memory)
export ORLY_SERIAL_CACHE_EVENT_IDS=500000 # Max event IDs to cache (~16MB memory)
# Directory Spider (metadata sync from other relays)
export ORLY_DIRECTORY_SPIDER=true # Enable directory spider
export ORLY_DIRECTORY_SPIDER_INTERVAL=24h # How often to run
export ORLY_DIRECTORY_SPIDER_HOPS=3 # Max hops for relay discovery
# NIP-43 Relay Access Metadata
export ORLY_NIP43_ENABLED=true # Enable invite system
export ORLY_NIP43_INVITE_EXPIRY=24h # Invite code validity
# Authentication modes
export ORLY_AUTH_REQUIRED=false # Require auth for all requests
export ORLY_AUTH_TO_WRITE=false # Require auth only for writes
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-policy-config.go- Kind 12345 policy updates and kind 3 admin follow list handlinghandle-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, Neo4j, or WasmDB)factory_wasm.go- WebAssembly-specific factory (build tag:js && wasm)database.go- Badger implementation with cache tuning and optional 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/wasmdb/ - WebAssembly IndexedDB database backend
wasmdb.go- Main WasmDB implementation using IndexedDB- Uses
aperturerobotics/go-indexeddbfor IndexedDB bindings - Replicates Badger's index schema for full query compatibility
- Object stores map to index prefixes (evt, eid, kc-, pc-, etc.)
- Range queries use IndexedDB cursors with KeyRange bounds
- Build tag:
js && wasm
pkg/neo4j/ - Neo4j graph database backend with social graph support
neo4j.go- Main database implementationschema.go- Graph schema and index definitions (includes WoT extensions)query-events.go- REQ filter to Cypher translationsave-event.go- Event storage with relationship creationsocial-event-processor.go- Processes kinds 0, 3, 1984, 10000 for social graphWOT_SPEC.md- Web of Trust data model specification (NostrUser nodes, trust metrics)MODIFYING_SCHEMA.md- Guide for schema modifications
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
- Write-Only Validation: Size, age, tag, and expiry validations apply ONLY to write operations
- Read-Only Filtering:
read_allow,read_deny,privilegedapply ONLY to read operations - See
docs/POLICY_CONFIGURATION_REFERENCE.mdfor authoritative read vs write applicability - Dynamic Policy Hot Reload via Kind 12345 Events:
- Policy admins can update policy configuration without relay restart
- Kind 12345 events contain JSON policy in content field
- Validation-first approach: JSON validated before pausing message processing
- Message processing uses RWMutex: RLock for normal ops, Lock for policy updates
- Policy admin follow lists (kind 3) trigger immediate cache refresh
WriteAllowFollowsrule grants both read+write access to admin follows- Tag validation supports regex patterns per tag type
- Policy Rule Fields:
max_expiry_duration: ISO-8601 duration format (e.g., "P7D", "PT1H30M") for event expiry limitsprotected_required: Requires NIP-70 protected events (must have "-" tag)identifier_regex: Regex pattern for validating "d" tag identifiersfollows_whitelist_admins: Per-rule admin pubkeys whose follows are whitelistedwrite_allow/write_deny: Pubkey whitelist/blacklist for writing (write-only)read_allow/read_deny: Pubkey whitelist/blacklist for reading (read-only)privileged: Party-involved access control (read-only)read_allow_permissive: Override kind whitelist for READ access (global rule only)write_allow_permissive: Override kind whitelist for WRITE access (global rule only)
- See
docs/POLICY_USAGE_GUIDE.mdfor configuration examples - See
pkg/policy/README.mdfor quick reference
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
- Directory Spider (
directory.go):- Discovers relays by crawling kind 10002 (relay list) events
- Expands outward from seed pubkeys (whitelisted users) via hop distance
- Fetches metadata events (kinds 0, 3, 10000, 10002) from discovered relays
- Self-detection prevents querying own relay
- Configurable interval and max hops via
ORLY_DIRECTORY_SPIDER_*env vars
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 with advanced filtering, sprocket management, policy management, user admin, settings
- Event Browser: Enhanced filter system with kind, author, tag, and time range filters (replaced simple search)
- Policy Management Tab: JSON editor with validation, save publishes kind 12345 event
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
- Neo4j: Graph database with social graph and Web of Trust (WoT) extensions
- Processes kinds 0 (profile), 3 (contacts), 1984 (reports), 10000 (mute list) for social graph
- NostrUser nodes with trust metrics (influence, PageRank)
- FOLLOWS, MUTES, REPORTS relationships for WoT analysis
- See
pkg/neo4j/WOT_SPEC.mdfor full schema specification
- WasmDB: IndexedDB backend for WebAssembly builds
- Enables running ORLY in browser environments
- Full query compatibility with Badger's index schema
- Uses
aperturerobotics/go-indexeddbfor IndexedDB access - Build with
GOOS=js GOARCH=wasm go build
- 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 - CRITICAL RULES:
- Uses
go-simpler.org/envfor struct tags - ALL environment variables MUST be defined in
app/config/config.go- NEVER use
os.Getenv()directly in packages - always pass config via structs - NEVER parse environment variables outside of
app/config/ - This ensures all config options appear in
./orly helpoutput - Database backends receive config via
database.DatabaseConfigstruct - Use
GetDatabaseConfigValues()helper to extract DB config from app config
- NEVER use
- All config fields use
ORLY_prefix with struct tags defining defaults and usage - Supports XDG directories via
github.com/adrg/xdg - Default data directory:
~/.local/share/ORLY - Database-specific config (Neo4j, Badger) is passed via
DatabaseConfigstruct inpkg/database/factory.go
Constants - CRITICAL RULES:
- ALWAYS define named constants for values used more than a few times
- ALWAYS define named constants if multiple packages depend on the same value
- Constants shared across packages should be in a dedicated package (e.g.,
pkg/constants/) - Magic numbers and strings are forbidden - use named constants with clear documentation
- Example:
// BAD - magic number if timeout > 30 { // GOOD - named constant const DefaultTimeoutSeconds = 30 if timeout > DefaultTimeoutSeconds {
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 (CRITICAL - Read Carefully):
The nostr library (git.mleku.dev/mleku/nostr/encoders/tag) uses binary optimization for e and p tags. This is a common source of bugs when working with pubkeys and event IDs.
How Binary Encoding Works:
- 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)
- The
tag.Tfield contains[][]bytewhere each element may be binary or hex depending on tag type event.E.ID,event.E.Pubkey, andevent.E.Sigare always stored as fixed-size byte arrays ([32]byteor[64]byte)
NEVER Do This:
// WRONG: tag.T[1] may be 33-byte binary, not 64-char hex!
pubkey := string(tag.T[1]) // Results in garbage for binary-encoded tags
// WRONG: Will fail for binary-encoded e/p tags
pt, err := hex.Dec(string(pTag.Value()))
ALWAYS Do This:
// CORRECT: Use ValueHex() which handles both binary and hex formats
pubkey := string(pTag.ValueHex()) // Always returns lowercase hex
// CORRECT: For decoding to bytes
pt, err := hex.Dec(string(pTag.ValueHex()))
// CORRECT: For event.E fields (always binary, use hex.Enc)
pubkeyHex := hex.Enc(ev.Pubkey[:]) // Always produces lowercase hex
eventIDHex := hex.Enc(ev.ID[:])
sigHex := hex.Enc(ev.Sig[:])
Tag Methods Reference:
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 otherwisetag.Value()- Returns raw bytes DANGEROUS for e/p tags - may be binary
Hex Case Sensitivity:
- The hex encoder (
git.mleku.dev/mleku/nostr/encoders/hex) always produces lowercase hex - External sources may send uppercase hex (e.g.,
"ABCD..."instead of"abcd...") - When storing pubkeys/event IDs (especially in Neo4j), always normalize to lowercase
- Mixed case causes duplicate entities in graph databases
Neo4j-Specific Helpers (pkg/neo4j/hex_utils.go):
// ExtractPTagValue handles binary encoding and normalizes to lowercase
pubkey := ExtractPTagValue(pTag)
// ExtractETagValue handles binary encoding and normalizes to lowercase
eventID := ExtractETagValue(eTag)
// NormalizePubkeyHex handles both binary and uppercase hex
normalized := NormalizePubkeyHex(rawValue)
// IsValidHexPubkey validates 64-char hex
if IsValidHexPubkey(pubkey) { ... }
Files Most Affected by These Rules:
pkg/neo4j/save-event.go- Event storage with e/p tag handlingpkg/neo4j/social-event-processor.go- Social graph with p-tag extractionpkg/neo4j/query-events.go- Filter queries with tag matchingpkg/database/save-event.go- Badger event storagepkg/database/filter_utils.go- Tag normalization utilitiespkg/find/parser.go- FIND protocol parser with p-tag extraction
This optimization saves memory and enables faster comparisons in the database layer.
Interface Design - CRITICAL RULES:
Rule 1: ALL interfaces MUST be defined in pkg/interfaces/<name>/
- Interfaces provide isolation between packages and enable dependency inversion
- Keeping interfaces in a dedicated package prevents circular dependencies
- Each interface package should be minimal (just the interface, no implementations)
Rule 2: NEVER use type assertions with interface literals
- NEVER write
.(interface{ Method() Type })- this is non-idiomatic and unmaintainable - Interface literals cannot be documented, tested for satisfaction, or reused
- Example of WRONG approach:
// BAD - interface literal in type assertion if checker, ok := obj.(interface{ Check() bool }); ok { checker.Check() } - Example of CORRECT approach:
// GOOD - use defined interface from pkg/interfaces/ import "next.orly.dev/pkg/interfaces/checker" if c, ok := obj.(checker.Checker); ok { c.Check() }
Rule 3: Resolving Circular Dependencies
- If a circular dependency occurs when adding an interface, move the interface to
pkg/interfaces/ - The implementing type stays in its original package
- The consuming code imports only the interface package
- This pattern:
pkg/interfaces/foo/ <- interface definition (no dependencies) ↑ ↑ pkg/bar/ pkg/baz/ (implements) (consumes via interface)
Existing interfaces in pkg/interfaces/:
acl/- ACL and PolicyChecker interfacesneterr/- TimeoutError interface for network errorsresultiter/- Neo4jResultIterator for database resultsstore/- Storage-related interfacespublisher/- Event publishing interfacestyper/- Type identification interface
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 database (Badger backend)github.com/neo4j/neo4j-go-driver/v5- Neo4j driver (Neo4j backend)github.com/aperturerobotics/go-indexeddb- IndexedDB bindings (WasmDB backend)github.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 (see Logging section below)
Logging (lol.mleku.dev)
The project uses lol.mleku.dev (Log Of Location), a simple logging library that prints timestamps and source code locations.
Log Levels (lowest to highest verbosity)
| Level | Constant | Emoji | Usage |
|---|---|---|---|
| Off | Off |
(none) | Disables all logging |
| Fatal | Fatal |
☠️ | Unrecoverable errors, program exits |
| Error | Error |
🚨 | Errors that need attention |
| Warn | Warn |
⚠️ | Warnings, non-critical issues |
| Info | Info |
ℹ️ | General information (default) |
| Debug | Debug |
🔎 | Debug information for development |
| Trace | Trace |
👻 | Very detailed tracing, most verbose |
Environment Variable
Set log level via LOG_LEVEL environment variable:
export LOG_LEVEL=trace # Most verbose
export LOG_LEVEL=debug # Development debugging
export LOG_LEVEL=info # Default
export LOG_LEVEL=warn # Only warnings and errors
export LOG_LEVEL=error # Only errors
export LOG_LEVEL=off # Silent
Note: ORLY uses ORLY_LOG_LEVEL which is mapped to the underlying LOG_LEVEL.
Usage in Code
Import and use the log package:
import "lol.mleku.dev/log"
// Log methods (each has .Ln, .F, .S, .C variants)
log.T.F("trace: %s", msg) // Trace level - very detailed
log.D.F("debug: %s", msg) // Debug level
log.I.F("info: %s", msg) // Info level
log.W.F("warn: %s", msg) // Warning level
log.E.F("error: %s", msg) // Error level
log.F.F("fatal: %s", msg) // Fatal level
// Check errors (prints if error is not nil, returns bool)
import "lol.mleku.dev/chk"
if chk.E(err) { // chk.E = Error level check
return // Error was logged
}
if chk.D(err) { // chk.D = Debug level check
// ...
}
Log Printer Variants
Each level has these printer types:
.Ln(a...)- Print items with spaces between.F(format, a...)- Printf-style formatting.S(a...)- Spew dump (detailed struct output).C(func() string)- Lazy evaluation (only runs closure if level is enabled).Chk(error) bool- Returns true if error is not nil, logs if so.Err(format, a...) error- Logs and returns an error
Output Format
1764783029014485👻 message text /path/to/file.go:123
- Unix microsecond timestamp
- Level emoji
- Message text
- Source file:line location
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: Optional 512MB query result cache (disabled by default via
ORLY_QUERY_CACHE_DISABLED=true) with zstd level 9 compression reduces database load for repeated queries; enable withORLY_QUERY_CACHE_DISABLED=false - Filter Normalization: When query cache is enabled, 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
- Optional 512MB query result cache with zstd level 9 compression (disabled by default to reduce memory usage)
- Enable with
ORLY_QUERY_CACHE_DISABLED=false - 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
Compact Event Storage (Latest)
- Events stored with 5-byte serial references instead of 32-byte IDs/pubkeys
- Achieves up to 40% space savings on event data
- Serial cache for fast lookups (configurable via
ORLY_SERIAL_CACHE_PUBKEYSandORLY_SERIAL_CACHE_EVENT_IDS) - Automatic migration from legacy format (version 6)
- Cleanup removes redundant legacy storage after migration
- Storage stats available via
db.CompactStorageStats()anddb.LogCompactSavings()
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
Git Commit Message Format
When asked to "make a commit comment", generate a commit message following this standard format:
Structure:
- First line: 72 characters maximum, imperative mood summary
- Second line: Empty line
- Body: Bullet points describing each change in detail
- Optional: "Files modified:" section listing affected files
Example:
Fix directory spider tag loss: size limits and validation
- Increase WebSocket message size limit from 500KB to 10MB to prevent
truncation of large kind 3 follow list events (8000+ follows)
- Add validation in SaveEvent to reject kind 3 events without p tags
before storage, preventing malformed events from buggy relays
- Implement CleanupKind3WithoutPTags() to remove existing malformed
kind 3 events at startup
- Add enhanced logging showing tag count and event ID when rejecting
invalid kind 3 events for better observability
Files modified:
- app/handle-websocket.go: Increase DefaultMaxMessageSize to 10MB
- pkg/database/save-event.go: Add kind 3 validation with logging
- pkg/database/cleanup-kind3.go: New cleanup function
- app/main.go: Invoke cleanup at startup
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
Recent Features (v0.34.x)
Directory Spider
The directory spider (pkg/spider/directory.go) automatically discovers and syncs metadata from other relays:
- Crawls kind 10002 (relay list) events to discover relays
- Expands outward from seed pubkeys (whitelisted users) via configurable hop distance
- Fetches essential metadata events (kinds 0, 3, 10000, 10002)
- Self-detection prevents querying own relay
- Enable with
ORLY_DIRECTORY_SPIDER=true
Neo4j Social Graph Backend
The Neo4j backend (pkg/neo4j/) includes Web of Trust (WoT) extensions:
- Social Event Processor: Handles kinds 0, 3, 1984, 10000 for social graph management
- NostrUser nodes: Store profile data and trust metrics (influence, PageRank)
- Relationships: FOLLOWS, MUTES, REPORTS for social graph analysis
- WoT Schema: See
pkg/neo4j/WOT_SPEC.mdfor full specification - Schema Modifications: See
pkg/neo4j/MODIFYING_SCHEMA.mdfor how to update
WasmDB IndexedDB Backend
WebAssembly-compatible database backend (pkg/wasmdb/):
- Enables running ORLY in browser environments
- Uses IndexedDB as storage via
aperturerobotics/go-indexeddb - Full query compatibility with Badger's index schema
- Object stores map to index prefixes (evt, eid, kc-, pc-, etc.)
- Range queries use IndexedDB cursors with KeyRange bounds
- Build with
GOOS=js GOARCH=wasm go build
Policy System Enhancements
- Default-Permissive Model: Read and write are allowed by default unless restrictions are configured
- Write-Only Validation: Size, age, tag validations apply ONLY to writes
- Read-Only Filtering:
read_allow,read_follows_whitelist,privilegedapply ONLY to reads - Separate Follows Whitelists:
read_follows_whitelistandwrite_follows_whitelistfor fine-grained control - Permissive Mode Overrides:
read_allow_permissiveandwrite_allow_permissive(global rule only) override kind whitelist for independent read/write control - Scripts: Policy scripts execute ONLY for write operations
- Reference Documentation:
docs/POLICY_CONFIGURATION_REFERENCE.mdprovides authoritative read vs write applicability - See also:
pkg/policy/README.mdfor quick reference
Policy JSON Configuration Quick Reference
{
"default_policy": "allow|deny",
"kind": {
"whitelist": [1, 3, 4], // Only these kinds allowed
"blacklist": [4] // These kinds denied (ignored if whitelist set)
},
"global": {
// Rule fields applied to ALL events
"size_limit": 100000, // Max event size (bytes)
"content_limit": 50000, // Max content size (bytes)
"max_age_of_event": 86400, // Max age (seconds)
"max_age_event_in_future": 300, // Max future time (seconds)
"max_expiry_duration": "P7D", // ISO-8601 expiry limit
"must_have_tags": ["d", "t"], // Required tag keys
"protected_required": false, // Require NIP-70 "-" tag
"identifier_regex": "^[a-z0-9-]{1,64}$", // Regex for "d" tags
"tag_validation": {"t": "^[a-z0-9]+$"}, // Regex for any tag
"privileged": false, // READ-ONLY: party-involved check
"write_allow": ["pubkey_hex"], // Pubkeys allowed to write
"write_deny": ["pubkey_hex"], // Pubkeys denied from writing
"read_allow": ["pubkey_hex"], // Pubkeys allowed to read
"read_deny": ["pubkey_hex"], // Pubkeys denied from reading
"read_follows_whitelist": ["pubkey_hex"], // Pubkeys whose follows can read
"write_follows_whitelist": ["pubkey_hex"], // Pubkeys whose follows can write
"read_allow_permissive": false, // Override kind whitelist for reads
"write_allow_permissive": false, // Override kind whitelist for writes
"script": "/path/to/script.sh" // External validation script
},
"rules": {
"1": { /* Same fields as global, for kind 1 */ },
"30023": { /* Same fields as global, for kind 30023 */ }
},
"policy_admins": ["pubkey_hex"], // Can update via kind 12345
"owners": ["pubkey_hex"], // Full policy control
"policy_follow_whitelist_enabled": false // Enable legacy write_allow_follows
}
Access Control Summary:
| Restriction Field | Applies To | When Set |
|---|---|---|
read_allow |
READ | Only listed pubkeys can read |
read_deny |
READ | Listed pubkeys denied (if no read_allow) |
read_follows_whitelist |
READ | Named pubkeys + their follows can read |
read_allow_permissive |
READ | Overrides kind whitelist for reads (global only) |
write_allow |
WRITE | Only listed pubkeys can write |
write_deny |
WRITE | Listed pubkeys denied (if no write_allow) |
write_follows_whitelist |
WRITE | Named pubkeys + their follows can write |
write_allow_permissive |
WRITE | Overrides kind whitelist for writes (global only) |
privileged |
READ | Only author + p-tag recipients can read |
Nil Policy Error Handling:
- If
ORLY_POLICY_ENABLED=truebut the policy fails to load (nil policy), the relay will:- Log a FATAL error message indicating misconfiguration
- Return an error for all
CheckPolicycalls - Deny all events until the configuration is fixed
- This is a safety measure - a nil policy with policy enabled indicates configuration error
Authentication Modes
ORLY_AUTH_REQUIRED=true: Require authentication for ALL requestsORLY_AUTH_TO_WRITE=true: Require authentication only for writes (allow anonymous reads)
NIP-43 Relay Access Metadata
Invite-based access control system:
ORLY_NIP43_ENABLED=true: Enable invite system- Publishes kind 8000/8001 events for member changes
- Publishes kind 13534 membership list events
- Configurable invite expiry via
ORLY_NIP43_INVITE_EXPIRY
Documentation Index
| Document | Purpose |
|---|---|
docs/POLICY_CONFIGURATION_REFERENCE.md |
Authoritative policy config reference with read/write applicability |
docs/POLICY_USAGE_GUIDE.md |
Comprehensive policy system user guide |
pkg/policy/README.md |
Policy system quick reference |
pkg/neo4j/README.md |
Neo4j backend overview |
pkg/neo4j/WOT_SPEC.md |
Web of Trust schema specification |
pkg/neo4j/MODIFYING_SCHEMA.md |
How to modify Neo4j schema |
pkg/neo4j/TESTING.md |
Neo4j testing guide |
.claude/skills/cypher/SKILL.md |
Cypher query language skill for Neo4j |
readme.adoc |
Project README with feature overview |