summaryrefslogtreecommitdiffstats
path: root/nostr-grpc-relay-design.md
blob: 29e90c4a475fb7e9e80a8dab6c0c845fca899356 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
# Nostr gRPC Relay - Design Document

## Project Overview

A 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.

## Core Philosophy

- **Transport as Implementation Detail**: Business logic is protocol-agnostic
- **Binary-First Storage**: Events stored as protobuf, JSON generated on-demand for WebSocket clients
- **Verify Once, Trust Forever**: Signature verification happens at ingestion only (events are immutable)
- **Dual Protocol**: Full Nostr compatibility via WebSocket + extended features via gRPC

## Architecture

```
┌─────────────────────────────────────────────────────────┐
│                    Transport Layer                       │
│  ┌──────────────────┐       ┌──────────────────┐       │
│  │   WebSocket      │       │      gRPC        │       │
│  │  (Nostr JSON)    │       │   (Protobuf)     │       │
│  │  - NIP-01        │       │   - Native API   │       │
│  │  - NIP-09        │       │   - Extensions   │       │
│  │  - NIP-11        │       │   - Admin API    │       │
│  │  - NIP-42        │       │                  │       │
│  └────────┬─────────┘       └────────┬─────────┘       │
│           │                          │                  │
│           └──────────┬───────────────┘                  │
└──────────────────────┼──────────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────────┐
│                  Application Layer                       │
│  ┌─────────────────────────────────────────────────┐   │
│  │  Subscription Manager                            │   │
│  │  - Filter matching (shared across protocols)     │   │
│  │  - Active subscription tracking                  │   │
│  │  - Event fan-out to subscribers                  │   │
│  │  - Deduplication                                 │   │
│  └─────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────┐   │
│  │  Event Processor                                 │   │
│  │  - Signature validation (schnorr)                │   │
│  │  - Event ID verification (SHA256)                │   │
│  │  - Canonical JSON reconstruction                 │   │
│  │  - Rate limiting / spam filtering                │   │
│  └─────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────┐   │
│  │  Auth Manager                                    │   │
│  │  - NIP-42 (WebSocket auth)                      │   │
│  │  - gRPC auth (mTLS, JWT, API keys)              │   │
│  └─────────────────────────────────────────────────┘   │
└──────────────────────┬──────────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────────┐
│                   Storage Layer (SQLite)                 │
│  ┌─────────────────────────────────────────────────┐   │
│  │  Event Store                                     │   │
│  │  - Binary protobuf storage (primary)             │   │
│  │  - Compressed canonical JSON (for verification)  │   │
│  │  - Denormalized indexes (pubkey, kind, etc)     │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘
```

## Key Design Decisions

### 1. Binary-First Storage

**Why**: 
- 40% smaller storage footprint
- Zero serialization cost for gRPC clients
- Faster queries on indexed fields

**How**:
- Store events as protobuf blobs
- Maintain denormalized fields (pubkey, kind, created_at, tags) for indexing
- Store compressed canonical JSON alongside for signature verification

### 2. Verify Signatures Once

**Rationale**:
- Nostr events are immutable (content-addressed by hash)
- Once signature is verified at ingestion, it never needs re-checking
- Clients already trust relays for many things (filtering, availability)

**Implementation**:
- Verify signature during event ingestion
- Reject invalid events immediately
- Store only valid events with `signature_valid=true` metadata
- Clients can opt-in to receive canonical JSON for their own verification

### 3. Dual Storage for Correctness

Store both protobuf AND canonical JSON:
- **Protobuf** (`event_data`): Fast queries, native gRPC serving
- **Canonical JSON** (`canonical_json`): Exact bytes that were signed, compressed with zstd

**Storage overhead**: ~150 bytes per event (compressed JSON)
- 1M events = 150MB extra
- Worth it for correctness and debuggability

### 4. SQLite for Storage

**Why SQLite**:
- Complex queries (Nostr filters) map directly to SQL
- Proven in production Nostr relays (strfry, nostream)
- Built-in indexing and query optimization
- JSON support for tag queries
- WAL mode: 10k-50k reads/sec, 1k-5k writes/sec
- Pure Go driver available (`modernc.org/sqlite`)

**Why not BadgerDB/Pebble**:
- Would require building custom query engine
- Complex index management
- More implementation work for same result

## Protocol Buffers Schema

```protobuf
syntax = "proto3";
package nostr;

// Core Nostr event
message Event {
  string id = 1;
  string pubkey = 2;
  int64 created_at = 3;
  int32 kind = 4;
  repeated Tag tags = 5;
  string content = 6;
  string sig = 7;
  
  // Optional: only populated if client requests verification
  // Contains exact canonical JSON bytes that were signed
  optional bytes canonical_json = 8;
}

message Tag {
  repeated string values = 1;  // e.g., ["e", "event_id", "relay_url"]
}

// Nostr filter (REQ)
message Filter {
  repeated string ids = 1;
  repeated string authors = 2;     // pubkeys
  repeated int32 kinds = 3;
  repeated string e_tags = 4;      // #e tag values
  repeated string p_tags = 5;      // #p tag values
  int64 since = 6;
  int64 until = 7;
  int32 limit = 8;
  
  // Extension: support for arbitrary tag filters
  map<string, TagFilter> tag_filters = 9;
}

message TagFilter {
  repeated string values = 1;
}

// gRPC Services
service NostrRelay {
  // Publish a single event
  rpc PublishEvent(PublishEventRequest) returns (PublishEventResponse);
  
  // Subscribe to events matching filters (streaming)
  rpc Subscribe(SubscribeRequest) returns (stream Event);
  
  // Unsubscribe from an active subscription
  rpc Unsubscribe(UnsubscribeRequest) returns (Empty);
  
  // gRPC-specific: batch publish
  rpc PublishBatch(PublishBatchRequest) returns (PublishBatchResponse);
  
  // gRPC-specific: paginated query (non-streaming)
  rpc QueryEvents(QueryRequest) returns (QueryResponse);
  
  // Event counts (NIP-45)
  rpc CountEvents(CountRequest) returns (CountResponse);
}

message PublishEventRequest {
  Event event = 1;
}

message PublishEventResponse {
  bool accepted = 1;
  string message = 2;  // Error message or "duplicate" or "success"
  
  // Always include canonical JSON so client can verify
  // what the relay stored
  bytes canonical_json = 3;
}

message SubscribeRequest {
  repeated Filter filters = 1;
  
  // If true, include canonical_json in streamed Event messages
  // Allows client-side signature verification
  // Default: false (most clients trust the relay)
  bool include_canonical_json = 2;
  
  // Optional client-provided subscription ID for tracking
  string subscription_id = 3;
}

message UnsubscribeRequest {
  string subscription_id = 1;
}

message PublishBatchRequest {
  repeated Event events = 1;
}

message PublishBatchResponse {
  repeated PublishEventResponse results = 1;
}

message QueryRequest {
  repeated Filter filters = 1;
  bool include_canonical_json = 2;
  
  // Pagination
  string cursor = 3;      // Opaque cursor from previous response
  int32 page_size = 4;    // Default: 100
}

message QueryResponse {
  repeated Event events = 1;
  string next_cursor = 2;  // Empty if no more results
  int32 total_count = 3;   // Optional: total matching events
}

message CountRequest {
  repeated Filter filters = 1;
}

message CountResponse {
  int64 count = 1;
}

message Empty {}

// Admin service (optional, secured separately)
service RelayAdmin {
  rpc GetStats(Empty) returns (RelayStats);
  rpc GetConnections(Empty) returns (ConnectionList);
  rpc BanPublicKey(BanRequest) returns (Empty);
  rpc GetStorageInfo(Empty) returns (StorageStats);
}

message RelayStats {
  int64 total_events = 1;
  int64 total_subscriptions = 2;
  int64 connected_clients = 3;
  int64 events_per_second = 4;
  int64 uptime_seconds = 5;
}

message ConnectionList {
  repeated Connection connections = 1;
}

message Connection {
  string client_id = 1;
  string protocol = 2;  // "websocket" or "grpc"
  int64 connected_at = 3;
  int32 active_subscriptions = 4;
}

message BanRequest {
  string pubkey = 1;
  int64 until = 2;  // Unix timestamp, 0 for permanent
  string reason = 3;
}

message StorageStats {
  int64 total_bytes = 1;
  int64 total_events = 2;
  int64 db_size_bytes = 3;
}
```

## Database Schema (SQLite)

```sql
-- Main events table
CREATE TABLE events (
    -- Primary event data
    id TEXT PRIMARY KEY,
    event_data BLOB NOT NULL,           -- Protobuf binary
    canonical_json BLOB NOT NULL,       -- zstd compressed canonical JSON
    
    -- Denormalized fields for efficient querying
    pubkey TEXT NOT NULL,
    kind INTEGER NOT NULL,
    created_at INTEGER NOT NULL,        -- Unix timestamp
    content TEXT,                       -- For full-text search (optional)
    tags JSON,                          -- SQLite JSON for tag queries
    sig TEXT NOT NULL,
    
    -- Metadata
    deleted BOOLEAN DEFAULT 0,
    received_at INTEGER DEFAULT (unixepoch())
);

-- Critical indexes for Nostr query patterns
CREATE INDEX idx_pubkey_created 
    ON events(pubkey, created_at DESC) 
    WHERE deleted = 0;

CREATE INDEX idx_kind_created 
    ON events(kind, created_at DESC) 
    WHERE deleted = 0;

CREATE INDEX idx_created 
    ON events(created_at DESC) 
    WHERE deleted = 0;

-- For tag queries (#e, #p, etc)
CREATE INDEX idx_tags 
    ON events(tags) 
    WHERE deleted = 0;

-- Optional: full-text search on content
CREATE VIRTUAL TABLE events_fts USING fts5(
    id UNINDEXED,
    content,
    content=events,
    content_rowid=rowid
);

-- Deletion events (NIP-09)
CREATE TABLE deletions (
    event_id TEXT PRIMARY KEY,          -- ID of deletion event
    deleted_event_id TEXT NOT NULL,     -- ID of event being deleted
    pubkey TEXT NOT NULL,               -- Who requested deletion
    created_at INTEGER NOT NULL,
    FOREIGN KEY (deleted_event_id) REFERENCES events(id)
);

CREATE INDEX idx_deleted_event ON deletions(deleted_event_id);

-- Replaceable events tracking (NIP-16, NIP-33)
CREATE TABLE replaceable_events (
    kind INTEGER NOT NULL,
    pubkey TEXT NOT NULL,
    d_tag TEXT,                         -- For parameterized replaceable events
    current_event_id TEXT NOT NULL,
    created_at INTEGER NOT NULL,
    PRIMARY KEY (kind, pubkey, COALESCE(d_tag, '')),
    FOREIGN KEY (current_event_id) REFERENCES events(id)
);

-- Auth challenges (NIP-42)
CREATE TABLE auth_challenges (
    challenge TEXT PRIMARY KEY,
    created_at INTEGER NOT NULL,
    expires_at INTEGER NOT NULL,
    used BOOLEAN DEFAULT 0
);

-- Rate limiting
CREATE TABLE rate_limits (
    pubkey TEXT PRIMARY KEY,
    event_count INTEGER DEFAULT 0,
    window_start INTEGER NOT NULL,
    last_reset INTEGER DEFAULT (unixepoch())
);
```

## Data Flow

### Event Ingestion (WebSocket or gRPC)

```
1. Event arrives (JSON via WebSocket OR protobuf via gRPC)

2. Parse/Deserialize
   - WebSocket: Parse JSON
   - gRPC: Deserialize protobuf

3. Construct Canonical JSON
   - WebSocket: Already have it
   - gRPC: Generate from protobuf
   Format: [0, pubkey, created_at, kind, tags, content]

4. Verify Signature
   - Compute: SHA256(canonical_json)
   - Verify: schnorr_verify(sig, hash, pubkey)
   - If invalid: REJECT immediately

5. Convert to Protobuf (if from WebSocket)

6. Compress Canonical JSON
   - Use zstd compression
   - ~60% size reduction

7. Store in SQLite
   INSERT INTO events (
     id, event_data, canonical_json, 
     pubkey, kind, created_at, tags, ...
   ) VALUES (?, ?, ?, ?, ?, ?, ?)

8. Handle Special Cases
   - NIP-09 deletion: Mark target event as deleted
   - NIP-16 replaceable: Update replaceable_events table
   - NIP-33 parameterized: Handle d-tag

9. Notify Active Subscriptions
   - Check all active filters
   - Fan out to matching subscribers
```

### Subscription Flow (gRPC)

```
1. Client calls Subscribe(filters, include_canonical_json)

2. Build SQL Query
   - Convert Nostr filters to SQL WHERE clauses
   - Handle multiple filters with UNION
   - Add ORDER BY created_at DESC
   - Apply LIMIT

3. Execute Query
   SELECT 
     event_data,
     CASE WHEN ? THEN canonical_json ELSE NULL END
   FROM events
   WHERE <filter conditions>
   ORDER BY created_at DESC
   LIMIT ?

4. For Each Row:
   - Deserialize protobuf (event_data)
   - If client wants verification:
     * Decompress canonical_json
     * Add to Event.canonical_json field
   - Stream Event to client

5. Send EOSE (End of Stored Events)
   - Could be metadata in stream or final message

6. Keep Subscription Active
   - New matching events → stream to client
   - Client closes stream → cleanup subscription
```

### WebSocket Flow (Standard Nostr)

```
1. Client sends: ["REQ", "sub-id", {...filter...}]

2. Convert Nostr Filter to Internal Filter
   - Same as gRPC Subscribe

3. Execute Query (same as above)

4. For Each Event:
   - Query: SELECT event_data, canonical_json FROM events
   - Deserialize protobuf
   - Convert protobuf → JSON
   - Send: ["EVENT", "sub-id", {...json_event...}]

5. Send: ["EOSE", "sub-id"]

6. Keep Subscription Active
   - New events → convert to JSON → send EVENT message

7. Client sends: ["CLOSE", "sub-id"]
   - Cleanup subscription
```

## Implementation Phases

### Phase 1: Core Relay (WebSocket Only)
**Goal**: Working Nostr relay with binary storage

- [ ] SQLite setup with schema
- [ ] Event validation (ID, signature verification)
- [ ] Canonical JSON generation
- [ ] Basic storage (protobuf + compressed JSON)
- [ ] WebSocket server (NIP-01)
- [ ] Filter → SQL query conversion
- [ ] Subscription management
- [ ] Event fan-out to subscribers

**Deliverable**: Can use with any Nostr client

### Phase 2: gRPC Parity
**Goal**: Prove dual-protocol concept works

- [ ] Protocol Buffer definitions
- [ ] gRPC server setup
- [ ] Implement PublishEvent, Subscribe, Unsubscribe
- [ ] Share subscription manager between protocols
- [ ] Test gRPC clients can pub/sub

**Deliverable**: Same functionality via gRPC

### Phase 3: gRPC Extensions
**Goal**: Leverage gRPC advantages

- [ ] Batch publishing (PublishBatch)
- [ ] Pagination (QueryEvents with cursors)
- [ ] Event counts (CountEvents)
- [ ] Client-side verification support (canonical_json field)
- [ ] Performance optimizations

**Deliverable**: gRPC offers features WebSocket can't

### Phase 4: Advanced Features
**Goal**: Production-ready relay

- [ ] NIP-09 (event deletion)
- [ ] NIP-11 (relay info document)
- [ ] NIP-42 (authentication)
- [ ] Rate limiting
- [ ] Admin API (stats, bans, monitoring)
- [ ] Full-text search (optional)
- [ ] Metrics/observability (Prometheus)

**Deliverable**: Feature-complete relay

### Phase 5: Optimization
**Goal**: High performance at scale

- [ ] Connection pooling
- [ ] Event caching (in-memory hot events)
- [ ] Query optimization (EXPLAIN QUERY PLAN)
- [ ] Compression tuning
- [ ] Load testing (thousands of concurrent subscriptions)

## NIPs (Nostr Implementation Possibilities) Support

### Required (Phase 1)
- **NIP-01**: Basic protocol (EVENT, REQ, CLOSE)
  - Event structure
  - Filters
  - Subscriptions

### Recommended (Phase 4)
- **NIP-09**: Event deletion
- **NIP-11**: Relay information document (JSON at relay URL)
- **NIP-16**: Replaceable events (kind 0, 3, 41)
- **NIP-33**: Parameterized replaceable events (kind 30000-39999)
- **NIP-42**: Authentication
- **NIP-45**: Event counts

### Optional
- **NIP-40**: Expiration timestamp
- **NIP-50**: Search (full-text)
- **NIP-65**: Relay list metadata

## Configuration

```yaml
# config.yaml
relay:
  name: "My gRPC Nostr Relay"
  description: "High-performance relay with gRPC support"
  pubkey: "relay_operator_pubkey"
  contact: "admin@example.com"

network:
  websocket:
    enabled: true
    host: "0.0.0.0"
    port: 8080
    max_connections: 10000
    
  grpc:
    enabled: true
    host: "0.0.0.0"
    port: 50051
    max_connections: 10000
    
  tls:
    enabled: false
    cert_file: "/path/to/cert.pem"
    key_file: "/path/to/key.pem"

storage:
  engine: "sqlite"
  path: "/var/lib/nostr-relay/relay.db"
  
  # SQLite-specific
  journal_mode: "WAL"
  synchronous: "NORMAL"
  cache_size: 10000  # pages
  
limits:
  max_event_size: 65536          # bytes
  max_subscriptions_per_client: 20
  max_filters_per_subscription: 10
  max_limit: 5000                # max events in one query
  
rate_limiting:
  enabled: true
  events_per_minute: 60
  window_size: 60                # seconds

retention:
  enabled: false
  max_age_days: 365              # Delete events older than this
  kinds_exempt: [0, 3]           # Don't delete these kinds

nips:
  supported: [1, 9, 11, 16, 33, 42, 45]
```

## Performance Targets

### Storage
- **1M events**: ~550 MB (protobuf + compressed JSON)
- **10M events**: ~5.5 GB
- **Compression ratio**: ~40% savings vs pure JSON

### Throughput
- **Writes**: 1,000-5,000 events/sec (single writer, SQLite WAL)
- **Reads**: 10,000-50,000 queries/sec (depends on query complexity)
- **Subscriptions**: Support 10,000+ concurrent subscriptions

### Latency
- **gRPC publish**: <5ms (signature verification dominates)
- **gRPC subscribe**: <2ms first event (hot path)
- **WebSocket publish**: <6ms (includes JSON→proto conversion)
- **WebSocket subscribe**: <3ms first event (includes proto→JSON)

## Testing Strategy

### Unit Tests
- Event validation (signature, ID)
- Canonical JSON generation
- Filter → SQL conversion
- Protobuf ↔ JSON conversion

### Integration Tests
- WebSocket protocol compliance
- gRPC service methods
- Concurrent subscriptions
- Event deletion and replacement
- Rate limiting

### Load Tests
- 1000 concurrent WebSocket connections
- 1000 concurrent gRPC streams
- Event ingestion at 1000/sec
- Memory usage under load

### Compatibility Tests
- Standard Nostr clients (Damus, Amethyst, Iris)
- gRPC clients (generated from .proto)
- Relay compatibility matrix

## Security Considerations

### Signature Verification
- Always verify schnorr signatures at ingestion
- Use well-tested crypto library (btcec, secp256k1)
- Reject malformed events immediately

### Rate Limiting
- Per-pubkey limits on event publishing
- Per-IP limits on connections
- Per-subscription limits on query complexity

### Authentication (NIP-42)
- Challenge-response auth for WebSocket
- Optional: require auth for publishing
- Configurable auth policies

### DoS Prevention
- Max event size (64KB default)
- Max subscriptions per client
- Max filters per subscription
- Max results per query
- Connection timeouts

### Input Validation
- Validate all event fields
- Sanitize SQL inputs (use parameterized queries)
- Limit JSON depth in tags
- Validate timestamps (not too far in future)

## Monitoring & Observability

### Metrics (Prometheus)
```
# Counters
nostr_events_received_total{protocol="websocket|grpc", valid="true|false"}
nostr_subscriptions_total{protocol="websocket|grpc"}
nostr_events_published_total
nostr_signature_verifications_total{valid="true|false"}

# Gauges
nostr_active_subscriptions{protocol="websocket|grpc"}
nostr_connected_clients{protocol="websocket|grpc"}
nostr_db_size_bytes
nostr_events_stored_total

# Histograms
nostr_event_publish_duration_seconds
nostr_query_duration_seconds
nostr_subscription_duration_seconds
```

### Logging
- Structured logging (JSON)
- Log levels: DEBUG, INFO, WARN, ERROR
- Include: timestamp, level, component, message, context

### Health Checks
- `/health` endpoint (HTTP)
- gRPC health service
- Database connectivity
- Disk space checks

## Deployment

### Docker
```dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /build
COPY . .
RUN go build -o relay cmd/relay/main.go

FROM alpine:latest
RUN apk --no-cache add ca-certificates
COPY --from=builder /build/relay /relay
COPY config.yaml /config.yaml
EXPOSE 8080 50051
CMD ["/relay", "--config", "/config.yaml"]
```

### Docker Compose
```yaml
version: '3.8'
services:
  relay:
    build: .
    ports:
      - "8080:8080"   # WebSocket
      - "50051:50051" # gRPC
    volumes:
      - ./data:/var/lib/nostr-relay
      - ./config.yaml:/config.yaml
    environment:
      - LOG_LEVEL=info
    restart: unless-stopped
```

### Systemd
```ini
[Unit]
Description=Nostr gRPC Relay
After=network.target

[Service]
Type=simple
User=nostr
WorkingDirectory=/opt/nostr-relay
ExecStart=/opt/nostr-relay/relay --config /etc/nostr-relay/config.yaml
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
```

## Project Structure

```
nostr-grpc-relay/
├── cmd/
│   └── relay/
│       └── main.go                 # Entry point
├── internal/
│   ├── config/
│   │   └── config.go              # Configuration loading
│   ├── storage/
│   │   ├── sqlite.go              # SQLite implementation
│   │   ├── schema.sql             # Database schema
│   │   └── queries.go             # SQL query builders
│   ├── nostr/
│   │   ├── event.go               # Event validation
│   │   ├── filter.go              # Filter handling
│   │   ├── signature.go           # Signature verification
│   │   └── canonical.go           # Canonical JSON generation
│   ├── subscription/
│   │   └── manager.go             # Subscription management
│   ├── transport/
│   │   ├── websocket/
│   │   │   └── server.go          # WebSocket handler
│   │   └── grpc/
│   │       └── server.go          # gRPC server
│   └── metrics/
│       └── prometheus.go          # Metrics collection
├── pkg/
│   └── pb/
│       └── nostr.proto            # Protocol Buffer definitions
├── test/
│   ├── integration/
│   └── load/
├── scripts/
│   └── setup-db.sh                # Database initialization
├── config.yaml                     # Default configuration
├── Dockerfile
├── docker-compose.yaml
├── go.mod
├── go.sum
└── README.md
```

## Dependencies

```go
// go.mod
module github.com/yourusername/nostr-grpc-relay

go 1.22

require (
    // Storage
    modernc.org/sqlite v1.28.0
    
    // gRPC & Protobuf
    google.golang.org/grpc v1.60.0
    google.golang.org/protobuf v1.32.0
    
    // WebSocket
    github.com/gorilla/websocket v1.5.1
    
    // Nostr crypto
    github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
    
    // Compression
    github.com/klauspost/compress v1.17.4
    
    // Configuration
    gopkg.in/yaml.v3 v3.0.1
    
    // Logging
    go.uber.org/zap v1.26.0
    
    // Metrics
    github.com/prometheus/client_golang v1.18.0
)
```

## Success Criteria

This project is successful if:

1. ✅ **Compatibility**: Any standard Nostr client can connect via WebSocket
2. ✅ **Performance**: gRPC clients get <2ms latency for cached queries
3. ✅ **Correctness**: 100% signature verification accuracy
4. ✅ **Reliability**: 99.9% uptime under normal load
5. ✅ **Scalability**: Handles 1M+ stored events efficiently
6. ✅ **Developer Experience**: gRPC clients are easier to build than WebSocket

## Future Extensions

### Phase 6+: Advanced Features
- **Multi-relay federation**: Sync events between relays via gRPC
- **Event compression**: Store events with better compression (Brotli, Zstandard dictionaries)
- **Sharding**: Distribute events across multiple SQLite databases by pubkey
- **Read replicas**: PostgreSQL with read replicas for massive scale
- **Graph queries**: Social graph analysis (who follows who, degrees of separation)
- **Machine learning**: Spam detection, content classification
- **Search**: Full-text search with ranking, faceted search

### gRPC-Specific Innovations
- **Streaming imports/exports**: Bulk event migration between relays
- **Transaction support**: Atomic multi-event operations
- **Server-side filtering**: Complex filters executed relay-side
- **Delta subscriptions**: Only send changed fields (bandwidth optimization)
- **Compression negotiation**: Per-client compression algorithms

## References

- [Nostr Protocol (NIPs)](https://github.com/nostr-protocol/nips)
- [gRPC Documentation](https://grpc.io/docs/)
- [Protocol Buffers Guide](https://protobuf.dev/)
- [SQLite Documentation](https://www.sqlite.org/docs.html)
- [Schnorr Signatures (BIP340)](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)

## Contact & Support

- **Issues**: GitHub Issues
- **Discussions**: GitHub Discussions
- **Nostr**: [Your Nostr Pubkey]

---

**License**: MIT

**Version**: 1.0.0-alpha

**Last Updated**: 2026-02-13