diff options
Diffstat (limited to 'internal/handler/websocket/index.go')
| -rw-r--r-- | internal/handler/websocket/index.go | 523 |
1 files changed, 115 insertions, 408 deletions
diff --git a/internal/handler/websocket/index.go b/internal/handler/websocket/index.go index 861fbb1..887b6ea 100644 --- a/internal/handler/websocket/index.go +++ b/internal/handler/websocket/index.go | |||
| @@ -10,441 +10,148 @@ var indexTemplate = template.Must(template.New("index").Parse(`<!DOCTYPE html> | |||
| 10 | <head> | 10 | <head> |
| 11 | <meta charset="UTF-8"> | 11 | <meta charset="UTF-8"> |
| 12 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | 12 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 13 | <title>NOSTR-GRPC-RELAY/v1.0.0</title> | 13 | <title>Nostr Relay</title> |
| 14 | <link rel="preconnect" href="https://fonts.googleapis.com"> | ||
| 15 | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | ||
| 16 | <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet"> | ||
| 17 | <style> | 14 | <style> |
| 18 | :root { | ||
| 19 | --bg-primary: #0a0e14; | ||
| 20 | --bg-secondary: #151a23; | ||
| 21 | --bg-tertiary: #1f2531; | ||
| 22 | --cyan: #00ff9f; | ||
| 23 | --green: #39ff14; | ||
| 24 | --white: #e6e6e6; | ||
| 25 | --gray: #6b7280; | ||
| 26 | --red: #ff3366; | ||
| 27 | --border: #2a3142; | ||
| 28 | } | ||
| 29 | |||
| 30 | * { | ||
| 31 | margin: 0; | ||
| 32 | padding: 0; | ||
| 33 | box-sizing: border-box; | ||
| 34 | } | ||
| 35 | |||
| 36 | body { | 15 | body { |
| 37 | font-family: 'JetBrains Mono', monospace; | 16 | font-family: 'Courier New', Courier, monospace; |
| 38 | background: var(--bg-primary); | 17 | background: #fff; |
| 39 | color: var(--white); | 18 | color: #000; |
| 40 | min-height: 100vh; | 19 | max-width: 720px; |
| 41 | overflow-x: hidden; | 20 | margin: 40px auto; |
| 42 | position: relative; | 21 | padding: 0 20px; |
| 43 | } | 22 | line-height: 1.6; |
| 44 | 23 | } | |
| 45 | /* Animated grid background */ | 24 | h1 { |
| 46 | body::before { | 25 | font-size: 18px; |
| 47 | content: ''; | 26 | font-weight: normal; |
| 48 | position: fixed; | 27 | margin: 20px 0 10px 0; |
| 49 | top: 0; | 28 | } |
| 50 | left: 0; | 29 | h2 { |
| 51 | width: 100%; | 30 | font-size: 16px; |
| 52 | height: 100%; | 31 | font-weight: normal; |
| 53 | background-image: | 32 | margin: 30px 0 10px 0; |
| 54 | repeating-linear-gradient(0deg, transparent, transparent 2px, var(--border) 2px, var(--border) 3px), | 33 | } |
| 55 | repeating-linear-gradient(90deg, transparent, transparent 2px, var(--border) 2px, var(--border) 3px); | 34 | h3 { |
| 56 | background-size: 50px 50px; | 35 | font-size: 14px; |
| 57 | opacity: 0.15; | 36 | font-weight: normal; |
| 58 | pointer-events: none; | 37 | margin: 20px 0 5px 0; |
| 59 | animation: gridScroll 20s linear infinite; | 38 | } |
| 60 | z-index: 0; | 39 | p, li { |
| 61 | } | 40 | font-size: 13px; |
| 62 | 41 | } | |
| 63 | @keyframes gridScroll { | 42 | a { |
| 64 | 0% { transform: translate(0, 0); } | 43 | color: #00e; |
| 65 | 100% { transform: translate(50px, 50px); } | 44 | text-decoration: underline; |
| 66 | } | 45 | } |
| 67 | 46 | a:visited { | |
| 68 | .container { | 47 | color: #551a8b; |
| 69 | position: relative; | 48 | } |
| 70 | z-index: 1; | 49 | hr { |
| 71 | max-width: 1200px; | 50 | border: none; |
| 72 | margin: 0 auto; | 51 | border-top: 1px solid #000; |
| 73 | padding: 2rem; | 52 | margin: 30px 0; |
| 74 | } | 53 | } |
| 75 | 54 | pre { | |
| 76 | /* Status bar */ | 55 | font-size: 12px; |
| 77 | .status-bar { | 56 | line-height: 1.4; |
| 78 | display: flex; | 57 | overflow-x: auto; |
| 79 | justify-content: space-between; | 58 | } |
| 80 | align-items: center; | 59 | code { |
| 81 | padding: 1rem; | 60 | font-family: 'Courier New', Courier, monospace; |
| 82 | background: var(--bg-secondary); | 61 | font-size: 13px; |
| 83 | border: 1px solid var(--border); | 62 | } |
| 84 | margin-bottom: 2rem; | 63 | .hash { |
| 85 | font-size: 0.75rem; | 64 | color: #666; |
| 86 | font-weight: 300; | 65 | font-size: 11px; |
| 87 | animation: fadeIn 0.5s ease-out; | 66 | } |
| 88 | } | 67 | ul { |
| 89 | 68 | padding-left: 20px; | |
| 90 | .status-item { | 69 | } |
| 91 | display: flex; | 70 | .pgp-block { |
| 92 | align-items: center; | 71 | margin: 20px 0; |
| 93 | gap: 0.5rem; | 72 | padding: 10px; |
| 94 | } | 73 | border: 1px solid #ccc; |
| 95 | 74 | background: #f5f5f5; | |
| 96 | .status-indicator { | ||
| 97 | width: 8px; | ||
| 98 | height: 8px; | ||
| 99 | background: var(--green); | ||
| 100 | border-radius: 50%; | ||
| 101 | animation: pulse 2s ease-in-out infinite; | ||
| 102 | } | ||
| 103 | |||
| 104 | @keyframes pulse { | ||
| 105 | 0%, 100% { opacity: 1; } | ||
| 106 | 50% { opacity: 0.4; } | ||
| 107 | } | ||
| 108 | |||
| 109 | /* Header with ASCII art */ | ||
| 110 | .header { | ||
| 111 | margin-bottom: 3rem; | ||
| 112 | animation: fadeIn 0.8s ease-out 0.2s both; | ||
| 113 | } | ||
| 114 | |||
| 115 | .ascii-logo { | ||
| 116 | font-size: 0.5rem; | ||
| 117 | line-height: 0.5rem; | ||
| 118 | color: var(--cyan); | ||
| 119 | white-space: pre; | ||
| 120 | margin-bottom: 1.5rem; | ||
| 121 | filter: drop-shadow(0 0 10px rgba(0, 255, 159, 0.3)); | ||
| 122 | animation: glitchLogo 3s ease-in-out infinite; | ||
| 123 | } | ||
| 124 | |||
| 125 | @keyframes glitchLogo { | ||
| 126 | 0%, 90%, 100% { transform: translate(0); } | ||
| 127 | 92% { transform: translate(-2px, 1px); } | ||
| 128 | 94% { transform: translate(2px, -1px); } | ||
| 129 | 96% { transform: translate(-1px, 2px); } | ||
| 130 | } | ||
| 131 | |||
| 132 | .header h1 { | ||
| 133 | font-size: 2rem; | ||
| 134 | font-weight: 700; | ||
| 135 | color: var(--cyan); | ||
| 136 | margin-bottom: 0.5rem; | ||
| 137 | letter-spacing: 0.1em; | ||
| 138 | } | ||
| 139 | |||
| 140 | .header .subtitle { | ||
| 141 | color: var(--gray); | ||
| 142 | font-size: 0.875rem; | ||
| 143 | font-weight: 300; | ||
| 144 | } | ||
| 145 | |||
| 146 | .header .subtitle::before { | ||
| 147 | content: '> '; | ||
| 148 | color: var(--green); | ||
| 149 | } | ||
| 150 | |||
| 151 | /* Protocol blocks */ | ||
| 152 | .protocols { | ||
| 153 | display: grid; | ||
| 154 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | ||
| 155 | gap: 1.5rem; | ||
| 156 | margin-bottom: 3rem; | ||
| 157 | } | ||
| 158 | |||
| 159 | .protocol-card { | ||
| 160 | background: var(--bg-secondary); | ||
| 161 | border: 1px solid var(--border); | ||
| 162 | padding: 1.5rem; | ||
| 163 | position: relative; | ||
| 164 | overflow: hidden; | ||
| 165 | animation: fadeInUp 0.8s ease-out backwards; | ||
| 166 | } | ||
| 167 | |||
| 168 | .protocol-card:nth-child(1) { animation-delay: 0.3s; } | ||
| 169 | .protocol-card:nth-child(2) { animation-delay: 0.4s; } | ||
| 170 | .protocol-card:nth-child(3) { animation-delay: 0.5s; } | ||
| 171 | |||
| 172 | .protocol-card::before { | ||
| 173 | content: ''; | ||
| 174 | position: absolute; | ||
| 175 | top: 0; | ||
| 176 | left: 0; | ||
| 177 | width: 100%; | ||
| 178 | height: 2px; | ||
| 179 | background: linear-gradient(90deg, var(--cyan), transparent); | ||
| 180 | animation: scanLine 2s ease-in-out infinite; | ||
| 181 | } | ||
| 182 | |||
| 183 | @keyframes scanLine { | ||
| 184 | 0%, 100% { transform: translateX(-100%); } | ||
| 185 | 50% { transform: translateX(100%); } | ||
| 186 | } | ||
| 187 | |||
| 188 | .protocol-card:hover { | ||
| 189 | border-color: var(--cyan); | ||
| 190 | box-shadow: 0 0 20px rgba(0, 255, 159, 0.1); | ||
| 191 | transition: all 0.3s ease; | ||
| 192 | } | ||
| 193 | |||
| 194 | .protocol-header { | ||
| 195 | display: flex; | ||
| 196 | justify-content: space-between; | ||
| 197 | align-items: center; | ||
| 198 | margin-bottom: 1rem; | ||
| 199 | } | ||
| 200 | |||
| 201 | .protocol-name { | ||
| 202 | font-size: 0.875rem; | ||
| 203 | font-weight: 700; | ||
| 204 | color: var(--green); | ||
| 205 | letter-spacing: 0.05em; | ||
| 206 | } | ||
| 207 | |||
| 208 | .protocol-status { | ||
| 209 | font-size: 0.75rem; | ||
| 210 | color: var(--gray); | ||
| 211 | display: flex; | ||
| 212 | align-items: center; | ||
| 213 | gap: 0.5rem; | ||
| 214 | } | ||
| 215 | |||
| 216 | .protocol-status::before { | ||
| 217 | content: '●'; | ||
| 218 | color: var(--green); | ||
| 219 | } | 75 | } |
| 76 | </style> | ||
| 77 | </head> | ||
| 78 | <body> | ||
| 220 | 79 | ||
| 221 | .protocol-endpoint { | 80 | <pre> |
| 222 | background: var(--bg-tertiary); | 81 | -----BEGIN NOSTR RELAY INFO----- |
| 223 | padding: 0.75rem; | 82 | Hash: SHA256 |
| 224 | margin: 1rem 0; | ||
| 225 | border-left: 2px solid var(--cyan); | ||
| 226 | font-size: 0.75rem; | ||
| 227 | color: var(--cyan); | ||
| 228 | word-break: break-all; | ||
| 229 | font-weight: 400; | ||
| 230 | } | ||
| 231 | 83 | ||
| 232 | .protocol-desc { | 84 | relay_id: nostr-grpc-relay |
| 233 | font-size: 0.75rem; | 85 | version: 1.0.0 |
| 234 | color: var(--gray); | 86 | status: online |
| 235 | font-weight: 300; | 87 | -----END NOSTR RELAY INFO----- |
| 236 | line-height: 1.5; | 88 | </pre> |
| 237 | } | ||
| 238 | 89 | ||
| 239 | /* Info section */ | 90 | <h1>Nostr Relay</h1> |
| 240 | .info-grid { | ||
| 241 | display: grid; | ||
| 242 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | ||
| 243 | gap: 1.5rem; | ||
| 244 | margin-bottom: 3rem; | ||
| 245 | } | ||
| 246 | 91 | ||
| 247 | .info-card { | 92 | <p>This relay implements the Nostr protocol with support for multiple transport layers. Connect using gRPC, HTTP/JSON (Connect), or standard WebSocket.</p> |
| 248 | background: var(--bg-secondary); | ||
| 249 | border: 1px solid var(--border); | ||
| 250 | padding: 1.5rem; | ||
| 251 | animation: fadeInUp 0.8s ease-out 0.6s backwards; | ||
| 252 | } | ||
| 253 | 93 | ||
| 254 | .info-card h3 { | 94 | <hr> |
| 255 | font-size: 0.75rem; | ||
| 256 | color: var(--cyan); | ||
| 257 | margin-bottom: 1rem; | ||
| 258 | letter-spacing: 0.1em; | ||
| 259 | font-weight: 700; | ||
| 260 | } | ||
| 261 | 95 | ||
| 262 | .nip-list { | 96 | <h2>Connection Endpoints</h2> |
| 263 | display: flex; | ||
| 264 | flex-wrap: wrap; | ||
| 265 | gap: 0.5rem; | ||
| 266 | } | ||
| 267 | 97 | ||
| 268 | .nip-tag { | 98 | <h3>gRPC (Native Binary)</h3> |
| 269 | background: var(--bg-tertiary); | 99 | <p><code>{{.GrpcAddr}}</code></p> |
| 270 | border: 1px solid var(--border); | 100 | <p>High-performance binary protocol using Protocol Buffers over HTTP/2. For applications requiring low latency and high throughput.</p> |
| 271 | padding: 0.25rem 0.75rem; | ||
| 272 | font-size: 0.75rem; | ||
| 273 | color: var(--green); | ||
| 274 | font-weight: 500; | ||
| 275 | transition: all 0.2s ease; | ||
| 276 | } | ||
| 277 | 101 | ||
| 278 | .nip-tag:hover { | 102 | <h3>Connect (HTTP/JSON)</h3> |
| 279 | border-color: var(--green); | 103 | <p><code>{{.HttpAddr}}/nostr.v1.NostrRelay/*</code></p> |
| 280 | box-shadow: 0 0 10px rgba(57, 255, 20, 0.2); | 104 | <p>Browser-compatible HTTP interface. Standard JSON over HTTP/1.1 or HTTP/2. CORS enabled.</p> |
| 281 | } | ||
| 282 | 105 | ||
| 283 | .feature-list { | 106 | <h3>WebSocket (Nostr Protocol)</h3> |
| 284 | list-style: none; | 107 | <p><code>ws://{{.WsAddr}}/</code></p> |
| 285 | font-size: 0.75rem; | 108 | <p>Standard Nostr protocol implementation (NIP-01). Compatible with all Nostr clients: Damus, Amethyst, Snort, Iris, etc.</p> |
| 286 | color: var(--gray); | ||
| 287 | font-weight: 300; | ||
| 288 | } | ||
| 289 | 109 | ||
| 290 | .feature-list li { | 110 | <hr> |
| 291 | padding: 0.5rem 0; | ||
| 292 | border-bottom: 1px solid var(--border); | ||
| 293 | } | ||
| 294 | 111 | ||
| 295 | .feature-list li:last-child { | 112 | <h2>Supported NIPs</h2> |
| 296 | border-bottom: none; | ||
| 297 | } | ||
| 298 | 113 | ||
| 299 | .feature-list li::before { | 114 | <ul> |
| 300 | content: '[✓] '; | 115 | <li><strong>NIP-01</strong> - Basic protocol flow (EVENT, REQ, CLOSE)</li> |
| 301 | color: var(--green); | 116 | <li><strong>NIP-09</strong> - Event deletion</li> |
| 302 | margin-right: 0.5rem; | 117 | <li><strong>NIP-11</strong> - Relay information document</li> |
| 303 | } | 118 | </ul> |
| 304 | 119 | ||
| 305 | /* Footer */ | 120 | <hr> |
| 306 | .footer { | ||
| 307 | text-align: center; | ||
| 308 | padding: 2rem; | ||
| 309 | border-top: 1px solid var(--border); | ||
| 310 | margin-top: 3rem; | ||
| 311 | font-size: 0.75rem; | ||
| 312 | color: var(--gray); | ||
| 313 | animation: fadeIn 0.8s ease-out 0.8s both; | ||
| 314 | } | ||
| 315 | 121 | ||
| 316 | .footer pre { | 122 | <h2>Implementation Details</h2> |
| 317 | margin: 1rem 0; | ||
| 318 | color: var(--cyan); | ||
| 319 | opacity: 0.5; | ||
| 320 | font-size: 0.5rem; | ||
| 321 | line-height: 0.6rem; | ||
| 322 | } | ||
| 323 | 123 | ||
| 324 | @keyframes fadeIn { | 124 | <p><strong>Storage:</strong> SQLite with Write-Ahead Logging (WAL mode)<br> |
| 325 | from { opacity: 0; } | 125 | <strong>Event Format:</strong> Binary-first (Protocol Buffers + compressed JSON)<br> |
| 326 | to { opacity: 1; } | 126 | <strong>Validation:</strong> Full cryptographic signature and event ID verification<br> |
| 327 | } | 127 | <strong>Subscriptions:</strong> Real-time event streaming with filter matching<br> |
| 128 | <strong>Deletion:</strong> Hard delete (events are permanently removed)</p> | ||
| 328 | 129 | ||
| 329 | @keyframes fadeInUp { | 130 | <hr> |
| 330 | from { | ||
| 331 | opacity: 0; | ||
| 332 | transform: translateY(20px); | ||
| 333 | } | ||
| 334 | to { | ||
| 335 | opacity: 1; | ||
| 336 | transform: translateY(0); | ||
| 337 | } | ||
| 338 | } | ||
| 339 | 131 | ||
| 340 | @media (max-width: 768px) { | 132 | <h2>Technical Notes</h2> |
| 341 | .container { | ||
| 342 | padding: 1rem; | ||
| 343 | } | ||
| 344 | .ascii-logo { | ||
| 345 | font-size: 0.35rem; | ||
| 346 | line-height: 0.4rem; | ||
| 347 | } | ||
| 348 | .header h1 { | ||
| 349 | font-size: 1.5rem; | ||
| 350 | } | ||
| 351 | .status-bar { | ||
| 352 | flex-direction: column; | ||
| 353 | gap: 0.5rem; | ||
| 354 | align-items: flex-start; | ||
| 355 | } | ||
| 356 | } | ||
| 357 | </style> | ||
| 358 | </head> | ||
| 359 | <body> | ||
| 360 | <div class="container"> | ||
| 361 | <div class="status-bar"> | ||
| 362 | <div class="status-item"> | ||
| 363 | <span class="status-indicator"></span> | ||
| 364 | <span>RELAY.ONLINE</span> | ||
| 365 | </div> | ||
| 366 | <div class="status-item"> | ||
| 367 | <span>PROTOCOLS: 3</span> | ||
| 368 | </div> | ||
| 369 | <div class="status-item"> | ||
| 370 | <span>NIPs: 01/09/11</span> | ||
| 371 | </div> | ||
| 372 | </div> | ||
| 373 | 133 | ||
| 374 | <div class="header"> | 134 | <p>Events are stored in binary format using Protocol Buffers alongside compressed canonical JSON. This dual-storage approach provides both performance (binary queries) and compatibility (JSON export).</p> |
| 375 | <pre class="ascii-logo"> | ||
| 376 | ███▄ █ ▒█████ ██████ ▄▄▄█████▓ ██▀███ | ||
| 377 | ██ ▀█ █ ▒██▒ ██▒▒██ ▒ ▓ ██▒ ▓▒▓██ ▒ ██▒ | ||
| 378 | ▓██ ▀█ ██▒▒██░ ██▒░ ▓██▄ ▒ ▓██░ ▒░▓██ ░▄█ ▒ | ||
| 379 | ▓██▒ ▐▌██▒▒██ ██░ ▒ ██▒░ ▓██▓ ░ ▒██▀▀█▄ | ||
| 380 | ▒██░ ▓██░░ ████▓▒░▒██████▒▒ ▒██▒ ░ ░██▓ ▒██▒ | ||
| 381 | ░ ▒░ ▒ ▒ ░ ▒░▒░▒░ ▒ ▒▓▒ ▒ ░ ▒ ░░ ░ ▒▓ ░▒▓░ | ||
| 382 | ░ ░░ ░ ▒░ ░ ▒ ▒░ ░ ░▒ ░ ░ ░ ░▒ ░ ▒░ | ||
| 383 | ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░ | ||
| 384 | ░ ░ ░ ░ ░ </pre> | ||
| 385 | <h1>NOSTR-GRPC-RELAY</h1> | ||
| 386 | <p class="subtitle">decentralized protocol node // multi-transport relay</p> | ||
| 387 | </div> | ||
| 388 | 135 | ||
| 389 | <div class="protocols"> | 136 | <p>The relay validates all events before storage:</p> |
| 390 | <div class="protocol-card"> | 137 | <pre>1. Verify event ID matches SHA256(canonical_json) |
| 391 | <div class="protocol-header"> | 138 | 2. Verify schnorr signature against pubkey |
| 392 | <span class="protocol-name">gRPC</span> | 139 | 3. Store in SQLite with indexed queries</pre> |
| 393 | <span class="protocol-status">ACTIVE</span> | ||
| 394 | </div> | ||
| 395 | <div class="protocol-endpoint">{{.GrpcAddr}}</div> | ||
| 396 | <p class="protocol-desc">Native binary protocol. High-throughput RPC interface for application-layer integration. Protocol Buffers over HTTP/2.</p> | ||
| 397 | </div> | ||
| 398 | 140 | ||
| 399 | <div class="protocol-card"> | 141 | <p>Subscriptions use a shared manager across all three protocols. An event published via gRPC will be immediately broadcast to WebSocket subscribers and vice versa.</p> |
| 400 | <div class="protocol-header"> | ||
| 401 | <span class="protocol-name">CONNECT</span> | ||
| 402 | <span class="protocol-status">ACTIVE</span> | ||
| 403 | </div> | ||
| 404 | <div class="protocol-endpoint">{{.HttpAddr}}/nostr.v1.NostrRelay/*</div> | ||
| 405 | <p class="protocol-desc">Browser-compatible HTTP/JSON interface. gRPC-Web with Connect protocol. CORS-enabled for web clients.</p> | ||
| 406 | </div> | ||
| 407 | 142 | ||
| 408 | <div class="protocol-card"> | 143 | <hr> |
| 409 | <div class="protocol-header"> | ||
| 410 | <span class="protocol-name">WEBSOCKET</span> | ||
| 411 | <span class="protocol-status">ACTIVE</span> | ||
| 412 | </div> | ||
| 413 | <div class="protocol-endpoint">ws://{{.WsAddr}}/</div> | ||
| 414 | <p class="protocol-desc">Standard Nostr protocol (NIP-01). Compatible with all Nostr clients. Real-time event streaming.</p> | ||
| 415 | </div> | ||
| 416 | </div> | ||
| 417 | 144 | ||
| 418 | <div class="info-grid"> | 145 | <div class="pgp-block"> |
| 419 | <div class="info-card"> | 146 | <pre>-----BEGIN PGP SIGNATURE----- |
| 420 | <h3>SUPPORTED_NIPS</h3> | 147 | <span class="hash">relay_fingerprint: a8f9c2e1d4b7a3f6</span> |
| 421 | <div class="nip-list"> | 148 | <span class="hash">protocol_version: nostr_01</span> |
| 422 | <span class="nip-tag">NIP-01</span> | 149 | <span class="hash">transport_types: grpc|connect|websocket</span> |
| 423 | <span class="nip-tag">NIP-09</span> | 150 | -----END PGP SIGNATURE-----</pre> |
| 424 | <span class="nip-tag">NIP-11</span> | 151 | </div> |
| 425 | </div> | ||
| 426 | </div> | ||
| 427 | 152 | ||
| 428 | <div class="info-card"> | 153 | <p><small>This relay runs on open-source software. Decentralized, censorship-resistant, user-owned.</small></p> |
| 429 | <h3>RELAY_FEATURES</h3> | ||
| 430 | <ul class="feature-list"> | ||
| 431 | <li>Binary-first storage</li> | ||
| 432 | <li>SQLite WAL mode</li> | ||
| 433 | <li>Event validation</li> | ||
| 434 | <li>Real-time subscriptions</li> | ||
| 435 | <li>Hard deletion (NIP-09)</li> | ||
| 436 | </ul> | ||
| 437 | </div> | ||
| 438 | </div> | ||
| 439 | 154 | ||
| 440 | <div class="footer"> | ||
| 441 | <pre> | ||
| 442 | ┌─────────────────────────────────────┐ | ||
| 443 | │ DECENTRALIZED // CENSORSHIP-RESISTANT │ | ||
| 444 | └─────────────────────────────────────┘</pre> | ||
| 445 | <p>relay.status = online // uptime.monitor = true</p> | ||
| 446 | </div> | ||
| 447 | </div> | ||
| 448 | </body> | 155 | </body> |
| 449 | </html>`)) | 156 | </html>`)) |
| 450 | 157 | ||
