summaryrefslogtreecommitdiffstats
path: root/nostr-grpc-relay-design.md
diff options
context:
space:
mode:
Diffstat (limited to 'nostr-grpc-relay-design.md')
-rw-r--r--nostr-grpc-relay-design.md901
1 files changed, 901 insertions, 0 deletions
diff --git a/nostr-grpc-relay-design.md b/nostr-grpc-relay-design.md
new file mode 100644
index 0000000..29e90c4
--- /dev/null
+++ b/nostr-grpc-relay-design.md
@@ -0,0 +1,901 @@
1# Nostr gRPC Relay - Design Document
2
3## Project Overview
4
5A high-performance Nostr relay implementation that speaks both the standard WebSocket protocol (for compatibility) and gRPC (for performance and developer experience). The relay is **gRPC-first**, storing events in binary Protocol Buffer format, with WebSocket/JSON as a compatibility layer.
6
7## Core Philosophy
8
9- **Transport as Implementation Detail**: Business logic is protocol-agnostic
10- **Binary-First Storage**: Events stored as protobuf, JSON generated on-demand for WebSocket clients
11- **Verify Once, Trust Forever**: Signature verification happens at ingestion only (events are immutable)
12- **Dual Protocol**: Full Nostr compatibility via WebSocket + extended features via gRPC
13
14## Architecture
15
16```
17┌─────────────────────────────────────────────────────────┐
18│ Transport Layer │
19│ ┌──────────────────┐ ┌──────────────────┐ │
20│ │ WebSocket │ │ gRPC │ │
21│ │ (Nostr JSON) │ │ (Protobuf) │ │
22│ │ - NIP-01 │ │ - Native API │ │
23│ │ - NIP-09 │ │ - Extensions │ │
24│ │ - NIP-11 │ │ - Admin API │ │
25│ │ - NIP-42 │ │ │ │
26│ └────────┬─────────┘ └────────┬─────────┘ │
27│ │ │ │
28│ └──────────┬───────────────┘ │
29└──────────────────────┼──────────────────────────────────┘
30
31┌──────────────────────▼──────────────────────────────────┐
32│ Application Layer │
33│ ┌─────────────────────────────────────────────────┐ │
34│ │ Subscription Manager │ │
35│ │ - Filter matching (shared across protocols) │ │
36│ │ - Active subscription tracking │ │
37│ │ - Event fan-out to subscribers │ │
38│ │ - Deduplication │ │
39│ └─────────────────────────────────────────────────┘ │
40│ ┌─────────────────────────────────────────────────┐ │
41│ │ Event Processor │ │
42│ │ - Signature validation (schnorr) │ │
43│ │ - Event ID verification (SHA256) │ │
44│ │ - Canonical JSON reconstruction │ │
45│ │ - Rate limiting / spam filtering │ │
46│ └─────────────────────────────────────────────────┘ │
47│ ┌─────────────────────────────────────────────────┐ │
48│ │ Auth Manager │ │
49│ │ - NIP-42 (WebSocket auth) │ │
50│ │ - gRPC auth (mTLS, JWT, API keys) │ │
51│ └─────────────────────────────────────────────────┘ │
52└──────────────────────┬──────────────────────────────────┘
53
54┌──────────────────────▼──────────────────────────────────┐
55│ Storage Layer (SQLite) │
56│ ┌─────────────────────────────────────────────────┐ │
57│ │ Event Store │ │
58│ │ - Binary protobuf storage (primary) │ │
59│ │ - Compressed canonical JSON (for verification) │ │
60│ │ - Denormalized indexes (pubkey, kind, etc) │ │
61│ └─────────────────────────────────────────────────┘ │
62└─────────────────────────────────────────────────────────┘
63```
64
65## Key Design Decisions
66
67### 1. Binary-First Storage
68
69**Why**:
70- 40% smaller storage footprint
71- Zero serialization cost for gRPC clients
72- Faster queries on indexed fields
73
74**How**:
75- Store events as protobuf blobs
76- Maintain denormalized fields (pubkey, kind, created_at, tags) for indexing
77- Store compressed canonical JSON alongside for signature verification
78
79### 2. Verify Signatures Once
80
81**Rationale**:
82- Nostr events are immutable (content-addressed by hash)
83- Once signature is verified at ingestion, it never needs re-checking
84- Clients already trust relays for many things (filtering, availability)
85
86**Implementation**:
87- Verify signature during event ingestion
88- Reject invalid events immediately
89- Store only valid events with `signature_valid=true` metadata
90- Clients can opt-in to receive canonical JSON for their own verification
91
92### 3. Dual Storage for Correctness
93
94Store both protobuf AND canonical JSON:
95- **Protobuf** (`event_data`): Fast queries, native gRPC serving
96- **Canonical JSON** (`canonical_json`): Exact bytes that were signed, compressed with zstd
97
98**Storage overhead**: ~150 bytes per event (compressed JSON)
99- 1M events = 150MB extra
100- Worth it for correctness and debuggability
101
102### 4. SQLite for Storage
103
104**Why SQLite**:
105- Complex queries (Nostr filters) map directly to SQL
106- Proven in production Nostr relays (strfry, nostream)
107- Built-in indexing and query optimization
108- JSON support for tag queries
109- WAL mode: 10k-50k reads/sec, 1k-5k writes/sec
110- Pure Go driver available (`modernc.org/sqlite`)
111
112**Why not BadgerDB/Pebble**:
113- Would require building custom query engine
114- Complex index management
115- More implementation work for same result
116
117## Protocol Buffers Schema
118
119```protobuf
120syntax = "proto3";
121package nostr;
122
123// Core Nostr event
124message Event {
125 string id = 1;
126 string pubkey = 2;
127 int64 created_at = 3;
128 int32 kind = 4;
129 repeated Tag tags = 5;
130 string content = 6;
131 string sig = 7;
132
133 // Optional: only populated if client requests verification
134 // Contains exact canonical JSON bytes that were signed
135 optional bytes canonical_json = 8;
136}
137
138message Tag {
139 repeated string values = 1; // e.g., ["e", "event_id", "relay_url"]
140}
141
142// Nostr filter (REQ)
143message Filter {
144 repeated string ids = 1;
145 repeated string authors = 2; // pubkeys
146 repeated int32 kinds = 3;
147 repeated string e_tags = 4; // #e tag values
148 repeated string p_tags = 5; // #p tag values
149 int64 since = 6;
150 int64 until = 7;
151 int32 limit = 8;
152
153 // Extension: support for arbitrary tag filters
154 map<string, TagFilter> tag_filters = 9;
155}
156
157message TagFilter {
158 repeated string values = 1;
159}
160
161// gRPC Services
162service NostrRelay {
163 // Publish a single event
164 rpc PublishEvent(PublishEventRequest) returns (PublishEventResponse);
165
166 // Subscribe to events matching filters (streaming)
167 rpc Subscribe(SubscribeRequest) returns (stream Event);
168
169 // Unsubscribe from an active subscription
170 rpc Unsubscribe(UnsubscribeRequest) returns (Empty);
171
172 // gRPC-specific: batch publish
173 rpc PublishBatch(PublishBatchRequest) returns (PublishBatchResponse);
174
175 // gRPC-specific: paginated query (non-streaming)
176 rpc QueryEvents(QueryRequest) returns (QueryResponse);
177
178 // Event counts (NIP-45)
179 rpc CountEvents(CountRequest) returns (CountResponse);
180}
181
182message PublishEventRequest {
183 Event event = 1;
184}
185
186message PublishEventResponse {
187 bool accepted = 1;
188 string message = 2; // Error message or "duplicate" or "success"
189
190 // Always include canonical JSON so client can verify
191 // what the relay stored
192 bytes canonical_json = 3;
193}
194
195message SubscribeRequest {
196 repeated Filter filters = 1;
197
198 // If true, include canonical_json in streamed Event messages
199 // Allows client-side signature verification
200 // Default: false (most clients trust the relay)
201 bool include_canonical_json = 2;
202
203 // Optional client-provided subscription ID for tracking
204 string subscription_id = 3;
205}
206
207message UnsubscribeRequest {
208 string subscription_id = 1;
209}
210
211message PublishBatchRequest {
212 repeated Event events = 1;
213}
214
215message PublishBatchResponse {
216 repeated PublishEventResponse results = 1;
217}
218
219message QueryRequest {
220 repeated Filter filters = 1;
221 bool include_canonical_json = 2;
222
223 // Pagination
224 string cursor = 3; // Opaque cursor from previous response
225 int32 page_size = 4; // Default: 100
226}
227
228message QueryResponse {
229 repeated Event events = 1;
230 string next_cursor = 2; // Empty if no more results
231 int32 total_count = 3; // Optional: total matching events
232}
233
234message CountRequest {
235 repeated Filter filters = 1;
236}
237
238message CountResponse {
239 int64 count = 1;
240}
241
242message Empty {}
243
244// Admin service (optional, secured separately)
245service RelayAdmin {
246 rpc GetStats(Empty) returns (RelayStats);
247 rpc GetConnections(Empty) returns (ConnectionList);
248 rpc BanPublicKey(BanRequest) returns (Empty);
249 rpc GetStorageInfo(Empty) returns (StorageStats);
250}
251
252message RelayStats {
253 int64 total_events = 1;
254 int64 total_subscriptions = 2;
255 int64 connected_clients = 3;
256 int64 events_per_second = 4;
257 int64 uptime_seconds = 5;
258}
259
260message ConnectionList {
261 repeated Connection connections = 1;
262}
263
264message Connection {
265 string client_id = 1;
266 string protocol = 2; // "websocket" or "grpc"
267 int64 connected_at = 3;
268 int32 active_subscriptions = 4;
269}
270
271message BanRequest {
272 string pubkey = 1;
273 int64 until = 2; // Unix timestamp, 0 for permanent
274 string reason = 3;
275}
276
277message StorageStats {
278 int64 total_bytes = 1;
279 int64 total_events = 2;
280 int64 db_size_bytes = 3;
281}
282```
283
284## Database Schema (SQLite)
285
286```sql
287-- Main events table
288CREATE TABLE events (
289 -- Primary event data
290 id TEXT PRIMARY KEY,
291 event_data BLOB NOT NULL, -- Protobuf binary
292 canonical_json BLOB NOT NULL, -- zstd compressed canonical JSON
293
294 -- Denormalized fields for efficient querying
295 pubkey TEXT NOT NULL,
296 kind INTEGER NOT NULL,
297 created_at INTEGER NOT NULL, -- Unix timestamp
298 content TEXT, -- For full-text search (optional)
299 tags JSON, -- SQLite JSON for tag queries
300 sig TEXT NOT NULL,
301
302 -- Metadata
303 deleted BOOLEAN DEFAULT 0,
304 received_at INTEGER DEFAULT (unixepoch())
305);
306
307-- Critical indexes for Nostr query patterns
308CREATE INDEX idx_pubkey_created
309 ON events(pubkey, created_at DESC)
310 WHERE deleted = 0;
311
312CREATE INDEX idx_kind_created
313 ON events(kind, created_at DESC)
314 WHERE deleted = 0;
315
316CREATE INDEX idx_created
317 ON events(created_at DESC)
318 WHERE deleted = 0;
319
320-- For tag queries (#e, #p, etc)
321CREATE INDEX idx_tags
322 ON events(tags)
323 WHERE deleted = 0;
324
325-- Optional: full-text search on content
326CREATE VIRTUAL TABLE events_fts USING fts5(
327 id UNINDEXED,
328 content,
329 content=events,
330 content_rowid=rowid
331);
332
333-- Deletion events (NIP-09)
334CREATE TABLE deletions (
335 event_id TEXT PRIMARY KEY, -- ID of deletion event
336 deleted_event_id TEXT NOT NULL, -- ID of event being deleted
337 pubkey TEXT NOT NULL, -- Who requested deletion
338 created_at INTEGER NOT NULL,
339 FOREIGN KEY (deleted_event_id) REFERENCES events(id)
340);
341
342CREATE INDEX idx_deleted_event ON deletions(deleted_event_id);
343
344-- Replaceable events tracking (NIP-16, NIP-33)
345CREATE TABLE replaceable_events (
346 kind INTEGER NOT NULL,
347 pubkey TEXT NOT NULL,
348 d_tag TEXT, -- For parameterized replaceable events
349 current_event_id TEXT NOT NULL,
350 created_at INTEGER NOT NULL,
351 PRIMARY KEY (kind, pubkey, COALESCE(d_tag, '')),
352 FOREIGN KEY (current_event_id) REFERENCES events(id)
353);
354
355-- Auth challenges (NIP-42)
356CREATE TABLE auth_challenges (
357 challenge TEXT PRIMARY KEY,
358 created_at INTEGER NOT NULL,
359 expires_at INTEGER NOT NULL,
360 used BOOLEAN DEFAULT 0
361);
362
363-- Rate limiting
364CREATE TABLE rate_limits (
365 pubkey TEXT PRIMARY KEY,
366 event_count INTEGER DEFAULT 0,
367 window_start INTEGER NOT NULL,
368 last_reset INTEGER DEFAULT (unixepoch())
369);
370```
371
372## Data Flow
373
374### Event Ingestion (WebSocket or gRPC)
375
376```
3771. Event arrives (JSON via WebSocket OR protobuf via gRPC)
378
3792. Parse/Deserialize
380 - WebSocket: Parse JSON
381 - gRPC: Deserialize protobuf
382
3833. Construct Canonical JSON
384 - WebSocket: Already have it
385 - gRPC: Generate from protobuf
386 Format: [0, pubkey, created_at, kind, tags, content]
387
3884. Verify Signature
389 - Compute: SHA256(canonical_json)
390 - Verify: schnorr_verify(sig, hash, pubkey)
391 - If invalid: REJECT immediately
392
3935. Convert to Protobuf (if from WebSocket)
394
3956. Compress Canonical JSON
396 - Use zstd compression
397 - ~60% size reduction
398
3997. Store in SQLite
400 INSERT INTO events (
401 id, event_data, canonical_json,
402 pubkey, kind, created_at, tags, ...
403 ) VALUES (?, ?, ?, ?, ?, ?, ?)
404
4058. Handle Special Cases
406 - NIP-09 deletion: Mark target event as deleted
407 - NIP-16 replaceable: Update replaceable_events table
408 - NIP-33 parameterized: Handle d-tag
409
4109. Notify Active Subscriptions
411 - Check all active filters
412 - Fan out to matching subscribers
413```
414
415### Subscription Flow (gRPC)
416
417```
4181. Client calls Subscribe(filters, include_canonical_json)
419
4202. Build SQL Query
421 - Convert Nostr filters to SQL WHERE clauses
422 - Handle multiple filters with UNION
423 - Add ORDER BY created_at DESC
424 - Apply LIMIT
425
4263. Execute Query
427 SELECT
428 event_data,
429 CASE WHEN ? THEN canonical_json ELSE NULL END
430 FROM events
431 WHERE <filter conditions>
432 ORDER BY created_at DESC
433 LIMIT ?
434
4354. For Each Row:
436 - Deserialize protobuf (event_data)
437 - If client wants verification:
438 * Decompress canonical_json
439 * Add to Event.canonical_json field
440 - Stream Event to client
441
4425. Send EOSE (End of Stored Events)
443 - Could be metadata in stream or final message
444
4456. Keep Subscription Active
446 - New matching events → stream to client
447 - Client closes stream → cleanup subscription
448```
449
450### WebSocket Flow (Standard Nostr)
451
452```
4531. Client sends: ["REQ", "sub-id", {...filter...}]
454
4552. Convert Nostr Filter to Internal Filter
456 - Same as gRPC Subscribe
457
4583. Execute Query (same as above)
459
4604. For Each Event:
461 - Query: SELECT event_data, canonical_json FROM events
462 - Deserialize protobuf
463 - Convert protobuf → JSON
464 - Send: ["EVENT", "sub-id", {...json_event...}]
465
4665. Send: ["EOSE", "sub-id"]
467
4686. Keep Subscription Active
469 - New events → convert to JSON → send EVENT message
470
4717. Client sends: ["CLOSE", "sub-id"]
472 - Cleanup subscription
473```
474
475## Implementation Phases
476
477### Phase 1: Core Relay (WebSocket Only)
478**Goal**: Working Nostr relay with binary storage
479
480- [ ] SQLite setup with schema
481- [ ] Event validation (ID, signature verification)
482- [ ] Canonical JSON generation
483- [ ] Basic storage (protobuf + compressed JSON)
484- [ ] WebSocket server (NIP-01)
485- [ ] Filter → SQL query conversion
486- [ ] Subscription management
487- [ ] Event fan-out to subscribers
488
489**Deliverable**: Can use with any Nostr client
490
491### Phase 2: gRPC Parity
492**Goal**: Prove dual-protocol concept works
493
494- [ ] Protocol Buffer definitions
495- [ ] gRPC server setup
496- [ ] Implement PublishEvent, Subscribe, Unsubscribe
497- [ ] Share subscription manager between protocols
498- [ ] Test gRPC clients can pub/sub
499
500**Deliverable**: Same functionality via gRPC
501
502### Phase 3: gRPC Extensions
503**Goal**: Leverage gRPC advantages
504
505- [ ] Batch publishing (PublishBatch)
506- [ ] Pagination (QueryEvents with cursors)
507- [ ] Event counts (CountEvents)
508- [ ] Client-side verification support (canonical_json field)
509- [ ] Performance optimizations
510
511**Deliverable**: gRPC offers features WebSocket can't
512
513### Phase 4: Advanced Features
514**Goal**: Production-ready relay
515
516- [ ] NIP-09 (event deletion)
517- [ ] NIP-11 (relay info document)
518- [ ] NIP-42 (authentication)
519- [ ] Rate limiting
520- [ ] Admin API (stats, bans, monitoring)
521- [ ] Full-text search (optional)
522- [ ] Metrics/observability (Prometheus)
523
524**Deliverable**: Feature-complete relay
525
526### Phase 5: Optimization
527**Goal**: High performance at scale
528
529- [ ] Connection pooling
530- [ ] Event caching (in-memory hot events)
531- [ ] Query optimization (EXPLAIN QUERY PLAN)
532- [ ] Compression tuning
533- [ ] Load testing (thousands of concurrent subscriptions)
534
535## NIPs (Nostr Implementation Possibilities) Support
536
537### Required (Phase 1)
538- **NIP-01**: Basic protocol (EVENT, REQ, CLOSE)
539 - Event structure
540 - Filters
541 - Subscriptions
542
543### Recommended (Phase 4)
544- **NIP-09**: Event deletion
545- **NIP-11**: Relay information document (JSON at relay URL)
546- **NIP-16**: Replaceable events (kind 0, 3, 41)
547- **NIP-33**: Parameterized replaceable events (kind 30000-39999)
548- **NIP-42**: Authentication
549- **NIP-45**: Event counts
550
551### Optional
552- **NIP-40**: Expiration timestamp
553- **NIP-50**: Search (full-text)
554- **NIP-65**: Relay list metadata
555
556## Configuration
557
558```yaml
559# config.yaml
560relay:
561 name: "My gRPC Nostr Relay"
562 description: "High-performance relay with gRPC support"
563 pubkey: "relay_operator_pubkey"
564 contact: "admin@example.com"
565
566network:
567 websocket:
568 enabled: true
569 host: "0.0.0.0"
570 port: 8080
571 max_connections: 10000
572
573 grpc:
574 enabled: true
575 host: "0.0.0.0"
576 port: 50051
577 max_connections: 10000
578
579 tls:
580 enabled: false
581 cert_file: "/path/to/cert.pem"
582 key_file: "/path/to/key.pem"
583
584storage:
585 engine: "sqlite"
586 path: "/var/lib/nostr-relay/relay.db"
587
588 # SQLite-specific
589 journal_mode: "WAL"
590 synchronous: "NORMAL"
591 cache_size: 10000 # pages
592
593limits:
594 max_event_size: 65536 # bytes
595 max_subscriptions_per_client: 20
596 max_filters_per_subscription: 10
597 max_limit: 5000 # max events in one query
598
599rate_limiting:
600 enabled: true
601 events_per_minute: 60
602 window_size: 60 # seconds
603
604retention:
605 enabled: false
606 max_age_days: 365 # Delete events older than this
607 kinds_exempt: [0, 3] # Don't delete these kinds
608
609nips:
610 supported: [1, 9, 11, 16, 33, 42, 45]
611```
612
613## Performance Targets
614
615### Storage
616- **1M events**: ~550 MB (protobuf + compressed JSON)
617- **10M events**: ~5.5 GB
618- **Compression ratio**: ~40% savings vs pure JSON
619
620### Throughput
621- **Writes**: 1,000-5,000 events/sec (single writer, SQLite WAL)
622- **Reads**: 10,000-50,000 queries/sec (depends on query complexity)
623- **Subscriptions**: Support 10,000+ concurrent subscriptions
624
625### Latency
626- **gRPC publish**: <5ms (signature verification dominates)
627- **gRPC subscribe**: <2ms first event (hot path)
628- **WebSocket publish**: <6ms (includes JSON→proto conversion)
629- **WebSocket subscribe**: <3ms first event (includes proto→JSON)
630
631## Testing Strategy
632
633### Unit Tests
634- Event validation (signature, ID)
635- Canonical JSON generation
636- Filter → SQL conversion
637- Protobuf ↔ JSON conversion
638
639### Integration Tests
640- WebSocket protocol compliance
641- gRPC service methods
642- Concurrent subscriptions
643- Event deletion and replacement
644- Rate limiting
645
646### Load Tests
647- 1000 concurrent WebSocket connections
648- 1000 concurrent gRPC streams
649- Event ingestion at 1000/sec
650- Memory usage under load
651
652### Compatibility Tests
653- Standard Nostr clients (Damus, Amethyst, Iris)
654- gRPC clients (generated from .proto)
655- Relay compatibility matrix
656
657## Security Considerations
658
659### Signature Verification
660- Always verify schnorr signatures at ingestion
661- Use well-tested crypto library (btcec, secp256k1)
662- Reject malformed events immediately
663
664### Rate Limiting
665- Per-pubkey limits on event publishing
666- Per-IP limits on connections
667- Per-subscription limits on query complexity
668
669### Authentication (NIP-42)
670- Challenge-response auth for WebSocket
671- Optional: require auth for publishing
672- Configurable auth policies
673
674### DoS Prevention
675- Max event size (64KB default)
676- Max subscriptions per client
677- Max filters per subscription
678- Max results per query
679- Connection timeouts
680
681### Input Validation
682- Validate all event fields
683- Sanitize SQL inputs (use parameterized queries)
684- Limit JSON depth in tags
685- Validate timestamps (not too far in future)
686
687## Monitoring & Observability
688
689### Metrics (Prometheus)
690```
691# Counters
692nostr_events_received_total{protocol="websocket|grpc", valid="true|false"}
693nostr_subscriptions_total{protocol="websocket|grpc"}
694nostr_events_published_total
695nostr_signature_verifications_total{valid="true|false"}
696
697# Gauges
698nostr_active_subscriptions{protocol="websocket|grpc"}
699nostr_connected_clients{protocol="websocket|grpc"}
700nostr_db_size_bytes
701nostr_events_stored_total
702
703# Histograms
704nostr_event_publish_duration_seconds
705nostr_query_duration_seconds
706nostr_subscription_duration_seconds
707```
708
709### Logging
710- Structured logging (JSON)
711- Log levels: DEBUG, INFO, WARN, ERROR
712- Include: timestamp, level, component, message, context
713
714### Health Checks
715- `/health` endpoint (HTTP)
716- gRPC health service
717- Database connectivity
718- Disk space checks
719
720## Deployment
721
722### Docker
723```dockerfile
724FROM golang:1.22-alpine AS builder
725WORKDIR /build
726COPY . .
727RUN go build -o relay cmd/relay/main.go
728
729FROM alpine:latest
730RUN apk --no-cache add ca-certificates
731COPY --from=builder /build/relay /relay
732COPY config.yaml /config.yaml
733EXPOSE 8080 50051
734CMD ["/relay", "--config", "/config.yaml"]
735```
736
737### Docker Compose
738```yaml
739version: '3.8'
740services:
741 relay:
742 build: .
743 ports:
744 - "8080:8080" # WebSocket
745 - "50051:50051" # gRPC
746 volumes:
747 - ./data:/var/lib/nostr-relay
748 - ./config.yaml:/config.yaml
749 environment:
750 - LOG_LEVEL=info
751 restart: unless-stopped
752```
753
754### Systemd
755```ini
756[Unit]
757Description=Nostr gRPC Relay
758After=network.target
759
760[Service]
761Type=simple
762User=nostr
763WorkingDirectory=/opt/nostr-relay
764ExecStart=/opt/nostr-relay/relay --config /etc/nostr-relay/config.yaml
765Restart=always
766RestartSec=10
767
768[Install]
769WantedBy=multi-user.target
770```
771
772## Project Structure
773
774```
775nostr-grpc-relay/
776├── cmd/
777│ └── relay/
778│ └── main.go # Entry point
779├── internal/
780│ ├── config/
781│ │ └── config.go # Configuration loading
782│ ├── storage/
783│ │ ├── sqlite.go # SQLite implementation
784│ │ ├── schema.sql # Database schema
785│ │ └── queries.go # SQL query builders
786│ ├── nostr/
787│ │ ├── event.go # Event validation
788│ │ ├── filter.go # Filter handling
789│ │ ├── signature.go # Signature verification
790│ │ └── canonical.go # Canonical JSON generation
791│ ├── subscription/
792│ │ └── manager.go # Subscription management
793│ ├── transport/
794│ │ ├── websocket/
795│ │ │ └── server.go # WebSocket handler
796│ │ └── grpc/
797│ │ └── server.go # gRPC server
798│ └── metrics/
799│ └── prometheus.go # Metrics collection
800├── pkg/
801│ └── pb/
802│ └── nostr.proto # Protocol Buffer definitions
803├── test/
804│ ├── integration/
805│ └── load/
806├── scripts/
807│ └── setup-db.sh # Database initialization
808├── config.yaml # Default configuration
809├── Dockerfile
810├── docker-compose.yaml
811├── go.mod
812├── go.sum
813└── README.md
814```
815
816## Dependencies
817
818```go
819// go.mod
820module github.com/yourusername/nostr-grpc-relay
821
822go 1.22
823
824require (
825 // Storage
826 modernc.org/sqlite v1.28.0
827
828 // gRPC & Protobuf
829 google.golang.org/grpc v1.60.0
830 google.golang.org/protobuf v1.32.0
831
832 // WebSocket
833 github.com/gorilla/websocket v1.5.1
834
835 // Nostr crypto
836 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
837
838 // Compression
839 github.com/klauspost/compress v1.17.4
840
841 // Configuration
842 gopkg.in/yaml.v3 v3.0.1
843
844 // Logging
845 go.uber.org/zap v1.26.0
846
847 // Metrics
848 github.com/prometheus/client_golang v1.18.0
849)
850```
851
852## Success Criteria
853
854This project is successful if:
855
8561. ✅ **Compatibility**: Any standard Nostr client can connect via WebSocket
8572. ✅ **Performance**: gRPC clients get <2ms latency for cached queries
8583. ✅ **Correctness**: 100% signature verification accuracy
8594. ✅ **Reliability**: 99.9% uptime under normal load
8605. ✅ **Scalability**: Handles 1M+ stored events efficiently
8616. ✅ **Developer Experience**: gRPC clients are easier to build than WebSocket
862
863## Future Extensions
864
865### Phase 6+: Advanced Features
866- **Multi-relay federation**: Sync events between relays via gRPC
867- **Event compression**: Store events with better compression (Brotli, Zstandard dictionaries)
868- **Sharding**: Distribute events across multiple SQLite databases by pubkey
869- **Read replicas**: PostgreSQL with read replicas for massive scale
870- **Graph queries**: Social graph analysis (who follows who, degrees of separation)
871- **Machine learning**: Spam detection, content classification
872- **Search**: Full-text search with ranking, faceted search
873
874### gRPC-Specific Innovations
875- **Streaming imports/exports**: Bulk event migration between relays
876- **Transaction support**: Atomic multi-event operations
877- **Server-side filtering**: Complex filters executed relay-side
878- **Delta subscriptions**: Only send changed fields (bandwidth optimization)
879- **Compression negotiation**: Per-client compression algorithms
880
881## References
882
883- [Nostr Protocol (NIPs)](https://github.com/nostr-protocol/nips)
884- [gRPC Documentation](https://grpc.io/docs/)
885- [Protocol Buffers Guide](https://protobuf.dev/)
886- [SQLite Documentation](https://www.sqlite.org/docs.html)
887- [Schnorr Signatures (BIP340)](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)
888
889## Contact & Support
890
891- **Issues**: GitHub Issues
892- **Discussions**: GitHub Discussions
893- **Nostr**: [Your Nostr Pubkey]
894
895---
896
897**License**: MIT
898
899**Version**: 1.0.0-alpha
900
901**Last Updated**: 2026-02-13