innis / nostr-relay
AMPHP-based async WebSocket relay server for Nostr protocol
Requires
- php: ^8.3
- amphp/amp: ^3.0
- amphp/http-server: ^3.0
- amphp/websocket-server: ^4.0
- innis/nostr-core: ^0.3
- psr/log: ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.85
- phpstan/phpstan: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^11.0
README
AMPHP-based async WebSocket relay server for Nostr protocol
A private, high-performance Nostr relay implementation designed to be embedded in PHP applications. Built with AMPHP for concurrent connection handling and clean architecture principles.
Features
- Interface-driven design - Storage and policies provided by host application
- AMPHP async - Non-blocking concurrent connections (100-1000+)
- Private relay focus - Built for single-user/controlled access scenarios
- NIP-01 compliant - EVENT, REQ, CLOSE message handling
- NIP-09 deletion - Kind 5 event processing
- NIP-11 support - Relay information document
- NIP-42 AUTH - Challenge/response authentication (challenge sent only once per client)
- NIP-45 COUNT - COUNT message support
- Ephemeral events - Kinds 20000-29999 skip storage
- Custom HTTP handlers - Inject handlers for additional HTTP endpoints (e.g. management APIs, landing pages)
- Mutable NIP-11 metadata - Swap in a custom
Nip11InfoProviderInterfaceto update relay info at runtime - Built-in RelayPolicy - Configurable tenant/guest permissions
- Real-time distribution - Events broadcast to matching subscriptions
- Rate limiting - DDoS protection with configurable limits; tenants (and trusted clients via
isRateLimitExempt()) bypass - CORS support -
OPTIONSpreflight handling and CORS headers on HTTP responses for browser-based clients - Trusted proxies -
X-Forwarded-Forhonoured when the client IP matches a trusted proxy - PSR-3 logging - Standard logging interface
Requirements
- PHP 8.3 or higher
innis/nostr-core- Core Nostr protocol entitiesamphp/amp^3.0 - Async runtimeamphp/http-server^3.0 - HTTP serveramphp/websocket-server^4.0 - WebSocket serverpsr/log^3.0 - Logging interface
Installation
composer require innis/nostr-relay
Quick Start
1. Implement Required Interfaces
The relay requires two interfaces to be implemented by your host application:
RelayEventStoreInterface- Event persistence and queriesRelayConfigInterface- Server and relay configuration
Access control can use the built-in RelayPolicy or a custom implementation of RelayPolicyInterface.
Two optional interfaces extend the relay's HTTP handling:
HttpRequestHandlerInterface- Handle additional HTTP requests (e.g. management API, landing page). Return a response ornullto fall through to WebSocket.Nip11InfoProviderInterface- Provide NIP-11 metadata dynamically. Defaults to reading fromRelayConfigInterfaceif not provided.
2. Create and Start the Relay
use Innis\Nostr\Relay\Application\Service\AuthenticationManager; use Innis\Nostr\Relay\Application\Service\RelayPolicy; use Innis\Nostr\Relay\Infrastructure\Server\RelayServerFactory; $authManager = new AuthenticationManager(); $logger = new \Psr\Log\NullLogger(); $policy = new RelayPolicy($authManager, $logger, [ 'tenants' => ['your-hex-pubkey'], 'guest' => [ 'read' => [ ['kinds' => [0, 1, 6, 7, 30023], 'from' => 'tenants'], ], 'write' => [ ['kinds' => [7, 9735]], ], ], ]); $factory = new RelayServerFactory( eventStore: new MyEventStore(), policy: $policy, config: new MyRelayConfig(), authManager: $authManager, logger: $logger, // Optional: custom HTTP handler for additional endpoints // httpHandler: new MyHttpHandler(), // Optional: dynamic NIP-11 metadata provider // nip11InfoProvider: new MyNip11InfoProvider(), ); $relay = $factory->create(); $relay->start();
See examples/relay.example.php for a complete working example with all interface implementations.
3. Configure Nginx
The relay does not handle TLS. Use a reverse proxy for SSL. If the proxy sets X-Forwarded-For, return its IP from RelayConfigInterface::getTrustedProxies() so the relay records the real client address:
upstream nostr_relay { server 127.0.0.1:8080; } server { listen 443 ssl; server_name relay.example.com; location / { proxy_pass http://nostr_relay; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_read_timeout 86400s; proxy_send_timeout 86400s; } }
Policy Configuration
The built-in RelayPolicy accepts a configuration array that controls access for tenants and guests.
Tenants
tenants: array of hex pubkeys or npub strings identifying relay owners. Tenants authenticate via NIP-42 and bypass all guest restrictions. If the array is empty or omitted, the relay operates as an open relay (all writes and reads allowed).
Limits
Optional keys with sensible defaults:
max_subscriptions- Maximum concurrent subscriptions per clientmax_filters- Maximum filters per subscriptionmax_event_size- Maximum event payload size in bytesmax_query_limit- Maximum limit value in REQ filters
Rate-Limit Exemption
RelayPolicyInterface::isRateLimitExempt() lets the policy opt specific clients out of rate limits and subscription caps. The built-in RelayPolicy exempts authenticated tenants (and everyone on an open relay). Implement RelayPolicyInterface directly to exempt other trusted clients — for example, internal services or IPs behind a trusted proxy.
Guest Rules
Unauthenticated clients are treated as guests. Guest permissions are defined under the guest key:
guest.read: array of rules controlling what events guests can query. Each rule has:
kinds(int array) - Event kinds the guest may readfrom(optional,'tenants') - Restrict results to events authored by tenants
guest.write: array of rules controlling what events guests can publish. Each rule has:
kinds(int array) - Event kinds the guest may publishtagged_to_tenant(optional,true) - Require the event to tag a tenant pubkey
If no config is passed, the relay is fully open with no restrictions.
Architecture
┌─────────────────────────────────────────────────────┐
│ Host Application │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ MyEventStore │ │ MyPolicy │ │ MyConfig │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└───────┬──────────────────┬───────────────┬──────────┘
│ │ │
┌───────▼──────────────────▼───────────────▼──────────┐
│ innis/nostr-relay │
│ │
│ WebSocket Server → Message Router → Use Cases │
│ │
│ SubscriptionManager → EventDistributor │
└─────────────────────────────────────────────────────┘
Relay Handles:
- WebSocket server lifecycle
- Connection management
- Message parsing (EVENT, REQ, CLOSE, AUTH, COUNT)
- NIP-42 authentication (challenge/response)
- NIP-09 deletion (kind 5 event processing)
- Ephemeral event handling (kinds 20000-29999)
- Subscription management and limits
- Filter matching and event distribution
- Rate limiting
Host Application Handles:
- Event storage and queries
- Access control policies (use built-in
RelayPolicyor implementRelayPolicyInterfacedirectly) - Server and NIP-11 configuration
- Custom HTTP endpoints (optional
HttpRequestHandlerInterface) - Runtime NIP-11 metadata (optional
Nip11InfoProviderInterface, defaults to static config)
Testing
composer test
Runs the unit test suite (120 tests) and PHPStan level 9 static analysis.
Manual testing with websocat:
websocat ws://localhost:8080 ["REQ","test",{"kinds":[1],"limit":10}]
Performance
Target Scale:
- 100-1000 concurrent WebSocket connections
- <10ms event distribution to 100 subscribers
- <5ms filter matching for 1000 subscriptions
- ~50 MB memory overhead for relay logic
Optimisations:
- AMPHP fibers for concurrent clients
- Subscription indexing by event kind
- Filter matching via nostr-core
- Non-blocking I/O throughout
License
MIT License. See LICENSE file for details.