# Aitherboard Specification A decentralized messageboard built on the Nostr protocol. This document defines the complete specification for implementation. This is a client from [silberengel@gitcitadel.com](https://jumble.imwald.eu/users/npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z) ## Table of Contents - [Technology Stack](#technology-stack) - [Project Structure](#project-structure) - [Event Types](#event-types) - [Relay Management](#relay-management) - [Authentication](#authentication) - [Content Modules](#content-modules) - [User Interface](#user-interface) - [Performance Requirements](#performance-requirements) - [Accessibility Requirements](#accessibility-requirements) - [Security Requirements](#security-requirements) - [Configuration](#configuration) - [Deployment](#deployment) ## Technology Stack **REQUIRED**: - Frontend: Svelte 5 (with runes: `$state`, `$derived`, `$effect`) + TypeScript + Vite - Styling: Tailwind CSS (4chan-style minimal design) - Nostr Library: [`nostr-tools`](https://github.com/nbd-wtf/nostr-tools) - Markdown: `marked` + `DOMPurify` for sanitization - Storage: IndexedDB (via `idb` or `localforage`) - Deployment: Docker + Apache httpd (static serving on port 9876) - PWA: Progressive Web App support ## Project Structure ``` aitherboard/ ├── src/ │ ├── lib/ │ │ ├── services/ │ │ │ ├── nostr/ │ │ │ │ ├── nostr-client.ts │ │ │ │ ├── auth-handler.ts │ │ │ │ └── config.ts │ │ │ ├── auth/ │ │ │ │ ├── nip07-signer.ts │ │ │ │ ├── nsec-signer.ts │ │ │ │ ├── bunker-signer.ts │ │ │ │ ├── anonymous-signer.ts │ │ │ │ ├── session-manager.ts │ │ │ │ ├── profile-fetcher.ts │ │ │ │ ├── relay-list-fetcher.ts │ │ │ │ ├── user-preferences-fetcher.ts │ │ │ │ ├── user-status-fetcher.ts │ │ │ │ └── activity-tracker.ts │ │ │ ├── cache/ │ │ │ │ ├── indexeddb-store.ts │ │ │ │ ├── event-cache.ts │ │ │ │ ├── profile-cache.ts │ │ │ │ ├── search-index.ts │ │ │ │ └── anonymous-key-store.ts │ │ │ └── security/ │ │ │ ├── key-management.ts │ │ │ ├── bech32-utils.ts │ │ │ ├── event-validator.ts │ │ │ └── sanitizer.ts │ │ ├── modules/ │ │ │ ├── threads/ │ │ │ ├── comments/ │ │ │ ├── zaps/ │ │ │ ├── reactions/ │ │ │ ├── profiles/ │ │ │ └── feed/ │ │ └── components/ │ │ ├── content/ │ │ ├── interactions/ │ │ ├── layout/ │ │ ├── modals/ │ │ └── preferences/ │ └── routes/ ├── public/ ├── scripts/ ├── Dockerfile ├── docker-compose.yml ├── httpd.conf.template └── docker-entrypoint.sh ``` ## Event Types ### Supported Event Kinds | Kind | Name | NIP | Purpose | |------|------|-----|---------| | 0 | Metadata/Profile | 1 | User profile information | | 1 | Note/Feed Post | 10 | Twitter-like feed posts | | 7 | Reaction | 25 | Upvote/downvote/like | | 11 | Thread | 7D | Discussion threads | | 1111 | Comment | 22 | Threaded comments | | 9734 | Zap Request | 57 | Lightning payment request (not published to relays) | | 9735 | Zap Receipt | 57 | Lightning payment confirmation | | 10000 | Mute List | 51 | User mute preferences | | 10002 | Relay List | 65 | User relay preferences (inbox/outbox) | | 10006 | Blocked Relays | 51 | User blocked relay list | | 10432 | Local Relays | - | Local relay preferences (used alongside 10002) | | 10133 | Payment Targets | A3 | Payment address information (RFC-8905) | | 30315 | General User Status | 38 | General user status (NIP-38) | **Note**: Media attachments use tags defined in NIP-92 (imeta), NIP-94 (file attachments), and NIP-23 (image tags). ### Event Structure Examples #### Kind 0 (Metadata/Profile) ```json { "kind": 0, "content": "{\"name\":\"Alice\",\"about\":\"Developer\",\"picture\":\"https://example.com/alice.jpg\",\"website\":\"https://alice.example.com\",\"lud16\":\"alice@wallet.example.com\",\"nip05\":\"alice@example.com\"}", "tags": [ ["name", "Alice"], ["about", "Developer"], ["picture", "https://example.com/alice.jpg"], ["website", "https://alice.example.com"], ["lud16", "alice@wallet.example.com"], ["nip05", "alice@example.com"], ["nip05", "alice@otherexample.com"] ], "pubkey": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", "created_at": 1679673265, "id": "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446", "sig": "f2cb581a84ed10e4dc84937bd98e27acac71ab057255f6aa8dfa561808c981fe8870f4a03c1e3666784d82a9c802d3704e174371aa13d63e2aeaf24ff5374d9d" } ``` **REQUIREMENTS**: - Parse tags (preferred) OR fallback to stringified JSON content - Support multiple tags for: `lud16`, `website`, `nip05` - Extract `lud16` tags for payment address display #### Kind 1 (Note/Feed Post) ```json { "kind": 1, "content": "Hello nostr!", "tags": [ ["e", "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446", "wss://relay.example.com", "reply", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"], ["p", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"], ["root", "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446"], ["client", "Aitherboard"] ], "pubkey": "a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4", "created_at": 1679673300, "id": "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "sig": "77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d" } ``` **REQUIREMENTS**: - Support NIP-10 threading (`e`, `p`, `root`, `reply` tags) - Render markdown content - Flat threading display (no indentation) #### Kind 7 (Reaction) ```json { "kind": 7, "content": "+", "tags": [ ["e", "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446", "wss://relay.example.com", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"], ["p", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", "wss://relay.example.com"], ["k", "1"], ["client", "Aitherboard"] ], "pubkey": "a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4", "created_at": 1679673300, "id": "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "sig": "77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d" } ``` **REQUIREMENTS**: See [Reaction Module](#reaction-module-srclibmodulesreactions) for full requirements. #### Kind 11 (Thread) ```json { "kind": 11, "content": "Good morning everyone!", "tags": [ ["title", "GM"], ["t", "tech"], ["image", "https://example.com/cover.jpg"], ["imeta", "url https://example.com/image1.jpg", "m image/jpeg", "x 1920", "y 1080"], ["imeta", "url https://example.com/video1.mp4", "m video/mp4", "x 1920", "y 1080", "dim 1920x1080x30"], ["file", "https://example.com/document.pdf", "application/pdf", "size 1048576"], ["client", "Aitherboard"] ], "pubkey": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", "created_at": 1679673265, "id": "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446", "sig": "f2cb581a84ed10e4dc84937bd98e27acac71ab057255f6aa8dfa561808c981fe8870f4a03c1e3666784d82a9c802d3704e174371aa13d63e2aeaf24ff5374d9d" } ``` **REQUIREMENTS**: - Max 3 `t` tags (topics) - Threads without topics appear under "General" - 30-day timeout (configurable) - Render markdown content - **Cover image**: Support `image` tag (NIP-23) for thread cover image - **Media attachments**: Support NIP-92 (imeta) and NIP-94 (file attachments) tags #### Kind 1111 (Comment) ```json { "kind": 1111, "content": "Cool beans", "tags": [ ["K", "11"], ["E", "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446", "wss://relay.example.com", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"], ["P", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"], ["e", "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446", "wss://relay.example.com", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"], ["k", "11"], ["p", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"], ["client", "Aitherboard"] ], "pubkey": "a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4", "created_at": 1679673300, "id": "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "sig": "77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d" } ``` **REQUIREMENTS**: - Parse NIP-22 tags: `K`, `E`, `e`, `k`, `p`, `P` - Flat list display (no indentation) - Gray blurb showing parent preview - Click blurb to highlight/scroll to parent - Render markdown content #### Kind 9734 (Zap Request) ```json { "kind": 9734, "content": "Zap!", "tags": [ ["relays", "wss://nostr-pub.wellorder.com", "wss://anotherrelay.example.com"], ["amount", "21000"], ["lnurl", "lnurl1dp68gurn8ghj7um5v93kketj9ehx2amn9uh8wetvdskkkmn0wahz7mrww4excup0dajx2mrv92x9xp"], ["p", "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9"], ["e", "9ae37aa68f48645127299e9453eb5d908a0cbb6058ff340d528ed4d37c8994fb"], ["k", "1"], ["client", "Aitherboard"] ], "pubkey": "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322", "created_at": 1679673265, "id": "30efed56a035b2549fcaeec0bf2c1595f9a9b3bb4b1a38abaf8ee9041c4b7d93", "sig": "f2cb581a84ed10e4dc84937bd98e27acac71ab057255f6aa8dfa561808c981fe8870f4a03c1e3666784d82a9c802d3704e174371aa13d63e2aeaf24ff5374d9d" } ``` **REQUIREMENTS**: - **NOT published to relays** - sent directly to wallet lnurl callback - **Primary method**: Send via `lightning:` URI to external wallet - **Fallback method**: Fetch invoice, display QR code + copyable text - User pays with external wallet (no in-app wallet) #### Kind 9735 (Zap Receipt) ```json { "id": "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446", "pubkey": "9630f464cca6a5147aa8a35f0bcdd3ce485324e732fd39e09233b1d848238f31", "created_at": 1674164545, "kind": 9735, "tags": [ ["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"], ["P", "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322"], ["e", "3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8"], ["k", "1"], ["bolt11", "lnbc10u1p3unwfusp5t9r3yymhpfqculx78u027lxspgxcr2n2987mx2j55nnfs95nxnzqpp5jmrh92pfld78spqs78v9euf2385t83uvpwk9ldrlvf6ch7tpascqhp5zvkrmemgth3tufcvflmzjzfvjt023nazlhljz2n9hattj4f8jq8qxqyjw5qcqpjrzjqtc4fc44feggv7065fqe5m4ytjarg3repr5j9el35xhmtfexc42yczarjuqqfzqqqqqqqqlgqqqqqqgq9q9qxpqysgq079nkq507a5tw7xgttmj4u990j7wfggtrasah5gd4ywfr2pjcn29383tphp4t48gquelz9z78p4cq7ml3nrrphw5w6eckhjwmhezhnqpy6gyf0"], ["description", "{\"pubkey\":\"97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322\",\"content\":\"\",\"id\":\"d9cc14d50fcb8c27539aacf776882942c1a11ea4472f8cdec1dea82fab66279d\",\"created_at\":1674164539,\"sig\":\"77127f636577e9029276be060332ea565deaf89ff215a494ccff16ae3f757065e2bc59b2e8c113dd407917a010b3abd36c8d7ad84c0e3ab7dab3a0b0caa9835d\",\"kind\":9734,\"tags\":[[\"e\",\"3624762a1274dd9636e0c552b53086d70bc88c165bc4dc0f9e836a1eaf86c3b8\"],[\"p\",\"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245\"],[\"relays\",\"wss://relay.damus.io\"]]}"], ["preimage", "5d006d2cf1e73c7148e7519a4c68adc81642ce0e25a432b2434c99f97344c15f"] ], "content": "", "sig": "f2cb581a84ed10e4dc84937bd98e27acac71ab057255f6aa8dfa561808c981fe8870f4a03c1e3666784d82a9c802d3704e174371aa13d63e2aeaf24ff5374d9d" } ``` **REQUIREMENTS**: - Display with ⚡ emoji - Only display if amount ≥ configured threshold (default: 1 sat) - Published by wallet, not client #### Kind 10002 (Relay List) ```json { "kind": 10002, "tags": [ ["r", "wss://alicerelay.example.com"], ["r", "wss://brando-relay.com"], ["r", "wss://expensive-relay.example2.com", "write"], ["r", "wss://nostr-relay.example.com", "read"] ], "content": "", "pubkey": "854043ae8f1f97430ca8c1f1a090bdde6488bd5115c7a45307a2a212750ae4cb", "created_at": 1699597889, "id": "a92a316b75e44cfdc19986c634049158d4206fcc0b7b9c7ccbcdabe28beebcd0", "sig": "1173822c53261f8cffe7efbf43ba4a97a9198b3e402c2a1df130f42a8985a2d0d3430f4de350db184141e45ca844ab4e5364ea80f11d720e36357e1853dba6ca" } ``` **REQUIREMENTS**: - Replaceable event (one per user) - `r` tags with relay URLs - Optional `read` or `write` marker (if omitted, relay is both) - **Inbox relays** (marked `read` or unmarked): Added to reading operations - **Outbox relays** (marked `write` or unmarked): Added to publishing operations - Used alongside kind 10432 (local relays) - both kinds combined for relay selection #### Kind 10432 (Local Relays) ```json { "kind": 10432, "tags": [ ["r", "ws://localhost:7777"], ["r", "ws://local-relay.example.com", "write"], ["r", "ws://localhost:8080", "read"] ], "content": "", "pubkey": "854043ae8f1f97430ca8c1f1a090bdde6488bd5115c7a45307a2a212750ae4cb", "created_at": 1699597889, "id": "a92a316b75e44cfdc19986c634049158d4206fcc0b7b9c7ccbcdabe28beebcd0", "sig": "1173822c53261f8cffe7efbf43ba4a97a9198b3e402c2a1df130f42a8985a2d0d3430f4de350db184141e45ca844ab4e5364ea80f11d720e36357e1853dba6ca" } ``` **REQUIREMENTS**: - Replaceable event (one per user) - `r` tags with relay URLs (typically local network relays) - Optional `read` or `write` marker (if omitted, relay is both) - **Inbox relays** (marked `read` or unmarked): Added to reading operations - **Outbox relays** (marked `write` or unmarked): Added to publishing operations - Used alongside kind 10002 - both kinds combined for relay selection - **NOTE**: Kind number 10432 should be verified against current Nostr specifications #### Kind 10000 (Mute List) ```json { "id": "a92a316b75e44cfdc19986c634049158d4206fcc0b7b9c7ccbcdabe28beebcd0", "pubkey": "854043ae8f1f97430ca8c1f1a090bdde6488bd5115c7a45307a2a212750ae4cb", "created_at": 1699597889, "kind": 10000, "tags": [ ["p", "07caba282f76441955b695551c3c5c742e5b9202a3784780f8086fdcdc1da3a9"], ["p", "a55c15f5e41d5aebd236eca5e0142789c5385703f1a7485aa4b38d94fd18dcc4"] ], "content": "", "sig": "1173822c53261f8cffe7efbf43ba4a97a9198b3e402c2a1df130f42a8985a2d0d3430f4de350db184141e45ca844ab4e5364ea80f11d720e36357e1853dba6ca" } ``` **REQUIREMENTS**: - Replaceable event (one per user) - Filter out all events from pubkeys in `p` tags - Applied automatically when user is logged in #### Kind 10006 (Blocked Relays) ```json { "kind": 10006, "content": "", "tags": [ ["relay", "wss://blocked-relay1.com"], ["relay", "wss://blocked-relay2.com"] ], "pubkey": "854043ae8f1f97430ca8c1f1a090bdde6488bd5115c7a45307a2a212750ae4cb", "created_at": 1699597889, "id": "a92a316b75e44cfdc19986c634049158d4206fcc0b7b9c7ccbcdabe28beebcd0", "sig": "1173822c53261f8cffe7efbf43ba4a97a9198b3e402c2a1df130f42a8985a2d0d3430f4de350db184141e45ca844ab4e5364ea80f11d720e36357e1853dba6ca" } ``` **REQUIREMENTS**: - Replaceable event (one per user) - Remove relays in `relay` tags from all operations (read and write) - Applied automatically when user is logged in - Filtered before establishing connections #### Kind 10133 (Payment Targets, NIP-A3) ```json { "kind": 10133, "content": "", "tags": [ ["payto", "bitcoin", "bc1qxq66e0t8d7ugdecwnmv58e90tpry23nc84pg9k"], ["payto", "nano", "nano_1dctqbmqxfppo9pswbm6kg9d4s4mbraqn8i4m7ob9gnzz91aurmuho48jx3c"], ["payto", "lightning", "user@wallet.example.com"] ], "pubkey": "afc93622eb4d79c0fb75e56e0c14553f7214b0a466abeba14cb38968c6755e6a", "created_at": 1679673265, "id": "67b48a14fb66c60c8f9070bdeb37afdfcc3d08ad01989460448e4081eddda446", "sig": "f2cb581a84ed10e4dc84937bd98e27acac71ab057255f6aa8dfa561808c981fe8870f4a03c1e3666784d82a9c802d3704e174371aa13d63e2aeaf24ff5374d9d" } ``` **REQUIREMENTS**: - Replaceable event (one per user) - Extract `payto` tags for payment address display - Combine with kind 0 `lud16` tags (deduplicated) - All addresses copyable - Lightning addresses zappable - Recognized types: bitcoin, lightning, ethereum, nano, monero, cashme, revolut, venmo - Unrecognized types displayed generically #### Kind 30315 (General User Status, NIP-38) ```json { "kind": 30315, "content": "Working on a new project", "tags": [ ["d", "general"], ["client", "Aitherboard"] ], "pubkey": "854043ae8f1f97430ca8c1f1a090bdde6488bd5115c7a45307a2a212750ae4cb", "created_at": 1699597889, "id": "a92a316b75e44cfdc19986c634049158d4206fcc0b7b9c7ccbcdabe28beebcd0", "sig": "1173822c53261f8cffe7efbf43ba4a97a9198b3e402c2a1df130f42a8985a2d0d3430f4de350db184141e45ca844ab4e5364ea80f11d720e36357e1853dba6ca" } ``` **REQUIREMENTS**: - Replaceable event (one per user) - Display status content on profile page - Fetch from same relays as kind 0 profiles - Defined in NIP-38 ## Relay Management ### Base Relay Lists | Category | Relays | Purpose | |----------|--------|---------| | **Default Relays** | `wss://theforest.nostr1.com`
`wss://nostr21.com`
`wss://nostr.land`
`wss://nostr.sovbit.host`
`wss://orly-relay.imwald.eu`
`wss://nostr.wine` | Base relays for all operations | | **Profile Relays** | `wss://relay.damus.io`
`wss://aggr.nostr.land`
`wss://profiles.nostr1.com` | Additional relays for profile/kind 1 content | ### Relay Selection by Operation | Operation | Relays Used | Notes | |-----------|------------|-------| | **Reading: Threads (kind 11)** | Default + User inbox (if logged in) | Blocked relays removed | | **Reading: Comments (kind 1111)** | Default + User inbox (if logged in) | Blocked relays removed | | **Reading: Kind 1 feed** | Default + User inbox (if logged in) | Blocked relays removed | | **Reading: Kind 1 responses** | Default + User inbox + `wss://aggr.nostr.land` | For threads beneath kind 1 OP events | | **Reading: Zap receipts (kind 9735)** | Default + User inbox (if logged in) | Blocked relays removed | | **Reading: Profiles (kind 0)** | Default + Profile + User inbox (if logged in) | Blocked relays removed | | **Reading: Payment targets (kind 10133)** | Default + Profile + User inbox (if logged in) | Blocked relays removed | | **Reading: User status (kind 30315)** | Default + Profile + User inbox (if logged in) | Blocked relays removed | | **Publishing: Threads (kind 11)** | Default + `wss://thecitadel.nostr1.com` + User outbox | Blocked relays removed | | **Publishing: Comments (kind 1111)** | Default + User outbox + Reply target's inbox | If replying, include target's inbox | | **Publishing: Kind 1 posts** | Default + User outbox + Reply target's inbox | If replying, include target's inbox | | **Publishing: Reactions (kind 7)** | Default + User outbox | Blocked relays removed | | **Publishing: Zap requests (kind 9734)** | Not published to relays | Sent to wallet lnurl callback | | **Publishing: Zap receipts (kind 9735)** | Relays from zap request | Published by wallet, not client | ### Dynamic Relay Management | Source | Kind | When Applied | Effect | |--------|------|--------------|--------| | **User Inbox Relays** | 10002 + 10432 (marked `read` or unmarked) | When user logs in | Added to reading operations | | **User Outbox Relays** | 10002 + 10432 (marked `write` or unmarked) | When user logs in | Added to publishing operations | | **Blocked Relays** | 10006 | When user logs in | Removed from all operations | **REQUIREMENTS**: - Relays combined, normalized, and deduplicated automatically - Relay list updates dynamically when user logs in/out - Blocked relays filtered before establishing connections - All operations respect user's kind 10002, kind 10432, and kind 10006 preferences - Kind 10002 and kind 10432 relays are combined (deduplicated) for each operation ## Authentication ### Authentication Methods | Method | Implementation | Key Storage | |--------|----------------|------------| | **NIP-07** | Browser extension (Alby, nos2x, etc.) | No storage (extension manages) | | **Nsec** | Direct bech32 nsec or hex private key, stored in the in-browser cache | **REQUIRED**: NIP-49 encrypted in localStorage | | **NIP-46 Bunker** | Remote signer via `bunker://` URI | No local storage | | **Anonymous** | Generated on the fly when publishing | **REQUIRED**: NIP-49 encrypted in IndexedDB | ### Key Storage & Encryption **CRITICAL**: NO SECRET KEYS STORED ON THE SERVER - All keys stored client-side only (IndexedDB/localStorage) - Server only serves static files - All key management in browser - **REQUIRED**: All nsec keys (including anonymous) MUST be encrypted with NIP-49 (password-based) before storage - Store as ncryptsec format (never plaintext nsec) - Anonymous keys persist in IndexedDB across sessions - Users can provide their own anonymous key (must be encrypted) ### Anonymous User Behavior - Pattern-based avatar - Handle: `Aitherite{random}` - Display with this format, whenever a kind 0 cannot be found, for the npub. ## Content Modules ### Event Publishing **REQUIREMENTS** (applies to all published events): - **NIP-89 client tag**: Add `["client", "Aitherboard"]` tag to all published events when checkbox is selected - Checkbox "Include client tag." displayed in all publish forms - Checkbox selected by default - Only include client tag if checkbox is selected (not deselected) - Applies to: kind 1, kind 7, kind 11, kind 1111 (all user-published events) ### Thread Module (`src/lib/modules/threads/`) **Components**: - `ThreadList.svelte` - Main thread listing - `ThreadCard.svelte` - Thread preview card (landing page) - `ThreadView.svelte` - Full thread display - `CreateThreadForm.svelte` - Thread creation **REQUIREMENTS**: - Max 3 `t` tags (topics) per thread - Threads without topics under "General" - 30-day timeout (configurable, checkbox to show older) - Sorting: newest, most active, most upvoted - Landing page: Title, relative times, profile badge, vote stats, first 250 chars plaintext (no markdown/images) - **Client badge**: Display client name from `client` tag (NIP-89) on thread cards - subtle, muted color - Render markdown in full thread view - **Cover image**: Display image from `image` tag (NIP-23) prominently on thread view - **Media attachments**: Handle NIP-92 (imeta) and NIP-94 (file) tags (see [Media Attachments](#media-attachments)) - Display all target relays with deselection option - Publication status modal: Success/failure per relay, error messages (hyperlink URLs), auto-close 30s or manual - Include NIP-89 client tag (see [Event Publishing](#event-publishing)) ### Comment Module (`src/lib/modules/comments/`) **Components**: - `Comment.svelte` - Individual comment - `CommentThread.svelte` - Threaded comment list - `CommentForm.svelte` - Reply form **REQUIREMENTS**: - Flat list (no indentation) - Gray blurb showing parent preview - Click blurb to highlight/scroll to parent - Parse NIP-22 tags: `K`, `E`, `e`, `k`, `p`, `P` - Render markdown content - **Client badge**: Display client name from `client` tag (NIP-89) on comment cards - subtle, muted color - Include NIP-89 client tag (see [Event Publishing](#event-publishing)) ### Zap Module (`src/lib/modules/zaps/`) **Components**: - `ZapButton.svelte` - Zap action button - `ZapReceipt.svelte` - Zap receipt display - `ZapInvoiceModal.svelte` - Invoice display (fallback) **REQUIREMENTS**: - **Primary**: Send zap request (kind 9734) via `lightning:` URI to external wallet - **Fallback**: Fetch invoice from lnurl callback, display QR code + copyable text - No in-app wallet connections - Display kind 9735 receipts (≥ configured threshold, default: 1 sat) - Display with ⚡ emoji - Zap requests NOT published to relays (sent to wallet) ### Reaction Module (`src/lib/modules/reactions/`) **Components**: - `ReactionButtons.svelte` - For threads/comments (kind 11/1111) - `FeedReactionButtons.svelte` - For kind 1 feed **REQUIREMENTS**: - **Kind 11/1111**: Only `+` and `-` allowed - `+` renders as upvote - `-` renders as downvote - **Backward compatibility**: `⬆️` (up arrow) counted as upvote, `⬇️` (down arrow) counted as downvote - **Kind 1**: All reactions allowed - `+` renders as ❤️ - Default suggested: `+` - Other reactions in submenu - One reaction per user per event - Clicking same reaction twice deletes it - Display reaction counts - Include NIP-89 client tag (see [Event Publishing](#event-publishing)) ### Profile Module (`src/lib/modules/profiles/`) **Components**: - `ProfilePage.svelte` - User profile display - `ProfileBadge.svelte` - Clickable profile badge with activity indicator and user status - `PaymentAddresses.svelte` - Unified payment addresses **REQUIREMENTS**: - **Kind 0**: Parse tags (preferred) OR fallback to JSON - Support multiple: `lud16`, `website`, `nip05` - Display: name, about, picture, NIP-05, website, lightning addresses - **Kind 30315 (NIP-38)**: Fetch and display general user status - Display status content on profile page - Display status in profile badges on cards (thread cards, comment cards, etc.) - Replaceable event (one per user) - **Payment addresses**: Fetch kind 10133, combine with kind 0 `lud16` - Deduplicate addresses - All addresses copyable - Lightning addresses zappable - Support recognized types: bitcoin, lightning, ethereum, nano, monero, cashme, revolut, venmo - Display unrecognized types generically - **Profile badge activity indicator**: See [Content Rendering](#content-rendering) section - Show kind 1 feed (see relay selection) - Show kind 1 responses (see relay selection) - Allow zapping profile owner - Allow replying to kind 1 with kind 1 or zap ### Kind 1 Feed Module (`src/lib/modules/feed/`) **Components**: - `FeedPage.svelte` - Main feed page - `FeedPost.svelte` - Individual kind 1 post - `FeedReply.svelte` - Kind 1 reply display - `ZapReceiptReply.svelte` - Zap receipt as reply (with ⚡) - `CreateFeedForm.svelte` - Create new kind 1 events - `ReplyToFeedForm.svelte` - Reply to kind 1 events **REQUIREMENTS**: - "View feed" button on landing page opens `/feed` - Fetch kind 1 events (see relay selection) - Create new kind 1 events with markdown editor - Reply to kind 1 with kind 1 (NIP-10 threading) - Reply to kind 1 with zap (see zap module) - Reply to kind 1 replies with kind 1 - Reply to zap receipts with zap receipts or kind 1111 comments - Display all reactions (not just upvotes) - Default suggested: `+` (rendered as ❤️) - Other reactions in submenu - **Client badge**: Display client name from `client` tag (NIP-89) on kind 1 post cards - subtle, muted color - Render all content as markdown - **Media attachments**: Handle NIP-92 (imeta) and NIP-94 (file) tags (see [Media Attachments](#media-attachments)) - Flat threading display (no indentation) - Real-time updates - Infinite scroll - Include NIP-89 client tag (see [Event Publishing](#event-publishing)) ## User Interface ### User Preferences & Readability **REQUIREMENTS**: - **Text size control**: Simple control (small/medium/large) to adjust base font size - Small: ~14px base - Medium: ~16px base (default) - Large: ~18px base - Preference stored in localStorage - Applied via CSS custom properties or data attributes - **Line spacing**: Adjustable line height for better readability - Tight: 1.4 - Normal: 1.6 (default) - Loose: 1.8 - **Content width**: Maximum content width control - Narrow: ~600px - Medium: ~800px (default) - Wide: ~1200px - **Theme**: Light/dark mode toggle - Support system preference detection (`prefers-color-scheme`) - Manual toggle to override system preference - Preference stored in localStorage - Smooth transition between modes - All preferences persist in localStorage - Preferences accessible from main navigation/settings menu - Clear visual feedback when preferences change ### Content Rendering **REQUIREMENTS**: - All content supports full markdown rendering - Videos MUST NOT autoplay (require user interaction) - Images lazy-loaded for performance - NIP-21 link parsing: - `nostr:npub...` → Clickable profile badge (with activity indicator and user status) - `nostr:nevent/note/naddr...` → Event card (250 char preview) → Event viewer - **Client badges**: Display client name from `client` tag (NIP-89) on all event cards - Display on: thread cards, comment cards, kind 1 post cards - Subtle, muted color styling - Small badge/chip format - **Profile badge**: - **Activity indicator** (custom feature, separate from kind 30315): - Track last activity from all events in relay pool per pubkey - **Red**: ≥168 hours (7 days) since last interaction - **Yellow**: ≥48 hours (2 days) but <168 hours since last interaction - **Green**: <48 hours since last interaction - Display indicator on all profile badges - **User status (kind 30315, NIP-38)**: Display user status text in profile badges on cards - **Content filtering**: Hide content with sensitive tags or #NSFW hashtag - Hide events with `content-warning` tag or `sensitive` tag - Hide events with `#NSFW` hashtag in content or tags - Content is completely hidden (not displayed with warning) - **Media handling**: See [Media Attachments](#media-attachments) section ### Media Attachments **REQUIREMENTS**: - **Support NIP-92 (imeta tags)**: Media metadata for images, videos, audio - Format: `["imeta", "url ", "m ", "x ", "y ", ...]` - Additional fields: `dim` (dimensions for video), `blurhash`, `thumb`, etc. - **Support NIP-94 (file tags)**: File attachments - Format: `["file", "", "", "size ", ...]` - **Support NIP-23 image tags**: Cover images for threads/articles - Format: `["image", ""]` - **Media URL normalization and deduplication**: - Extract media URLs from: - `imeta` tags (url field) - `file` tags (first element after "file") - `image` tags (second element) - Markdown image syntax in content: `![alt](url)` - HTML ``, `