You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Silberengel 3720edd127 virtual scrolling 1 month ago
.vscode first successful build 1 month ago
public virtual scrolling 1 month ago
scripts implement remaining MVP features 1 month ago
src virtual scrolling 1 month ago
static refactor 1 month ago
.eslintrc.cjs first phase 1 month ago
.gitignore first phase 1 month ago
.prettierrc first phase 1 month ago
Dockerfile fix relay management and caching 1 month ago
README.md fix lefotver opengraph data stores. more efficient caching 1 month ago
README_SETUP.md implement anon signing and nsec encryption+saving 1 month ago
docker-compose.yml fix relay management and caching 1 month ago
docker-entrypoint.sh fix relay management and caching 1 month ago
httpd.conf.template fix relay management and caching 1 month ago
package-lock.json virtual scrolling 1 month ago
package.json virtual scrolling 1 month ago
postcss.config.js first phase 1 month ago
svelte.config.js bug-fixes 1 month ago
tailwind.config.js implement anon signing and nsec encryption+saving 1 month ago
tsconfig.json first successful build 1 month ago
vite.config.js first successful build 1 month ago
vite.config.js.map first successful build 1 month ago
vite.config.ts refactor 1 month ago

README.md

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

Table of Contents

Technology Stack

REQUIRED:

  • Frontend: Svelte 5 (with runes: $state, $derived, $effect) + TypeScript + Vite
  • Styling: Tailwind CSS (4chan-style minimal design)
  • Nostr Library: 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)

{
  "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)

{
  "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)

{
  "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 for full requirements.

Kind 11 (Thread)

{
  "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)

{
  "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)

{
  "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)

{
  "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)

{
  "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)

{
  "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)

{
  "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)

{
  "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)

{
  "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)

{
  "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://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 IndexedDB
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 in IndexedDB
  • 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)
  • All encrypted 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)
  • 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)

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)

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)

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 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)
  • Flat threading display (no indentation)
  • Real-time updates
  • Infinite scroll
  • Include NIP-89 client tag (see 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 section

Media Attachments

REQUIREMENTS:

  • Support NIP-92 (imeta tags): Media metadata for images, videos, audio
    • Format: ["imeta", "url <URL>", "m <mime-type>", "x <width>", "y <height>", ...]
    • Additional fields: dim (dimensions for video), blurhash, thumb, etc.
  • Support NIP-94 (file tags): File attachments
    • Format: ["file", "<URL>", "<mime-type>", "size <bytes>", ...]
  • Support NIP-23 image tags: Cover images for threads/articles
    • Format: ["image", "<URL>"]
  • 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 <img>, <video>, <audio> tags in rendered markdown
    • Normalize URLs (remove query params, fragments, trailing slashes for comparison)
    • Deduplicate: Each unique URL displayed only once per event
    • Display order: image tag → imeta tags → file tags → content-extracted URLs
  • Display requirements:
    • Cover images (from image tag): Display prominently (e.g., at top of thread/article)
    • Images from imeta: Display in media gallery
    • Videos from imeta: Display with play button, no autoplay
    • Audio from imeta: Display with audio player controls
    • Files from file tags: Display as download links with file type icons
    • Content-extracted media: Display inline with markdown rendering

Thread Organization

REQUIREMENTS:

  • Threads sorted by t tags (topics)
  • Max 3 topics per thread
  • Threads without topics under "General"
  • Sorting: newest, most active, most upvoted
  • 30-day timeout (configurable, checkbox to show older)

Comment Threading

REQUIREMENTS:

  • Flat list (no indentation)
  • Gray blurb showing parent preview
  • Click blurb to highlight/scroll to parent
  • Parse NIP-22 tags for threading

Advanced Features

REQUIREMENTS:

  • Full-text search (keyboard shortcut /)
  • Local caching (IndexedDB) for offline access
  • Thread statistics (comment counts, zap totals, activity)
  • Real-time updates (live indicators)
  • Relay health monitoring (connection status, latency, auto-retry)
  • User muting: Fetch kind 10000, suppress events from listed pubkeys (implemented in event-store.ts)
  • Activity tracking: Track last activity timestamp per pubkey from all events in relay pool (implemented in event-store.ts and activity-tracker.ts)
  • User relay blocking: Fetch kind 10006, remove blocked relays (see relay management)
  • 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)
  • Thread locking (prevent replies to old/closed threads)
  • Keyboard shortcuts (j/k navigation, r reply, z zap, etc.)
  • Thread bumping (active threads rise to top)

Performance Requirements

REQUIREMENTS:

  • Aggressive caching: IndexedDB for offline access and faster loads
  • Lazy loading: Images and non-critical content on demand
  • Code splitting: Route-based to minimize initial bundle
  • Connection resilience: Graceful handling of slow/failed relays
  • Progressive loading: Show cached content immediately while fetching updates
  • Request batching: Batch relay queries to reduce overhead
  • Compression: Gzip/Brotli for all assets
  • CDN-ready: Static assets optimized for CDN
  • Timeout management: Configurable timeouts for relay connections
  • Retry logic: Automatic retry with exponential backoff
  • Connection pooling: Efficient WebSocket management
  • Graceful degradation: Show cached content when relays slow/unavailable
  • Loading states: Clear feedback during slow operations
  • Offline support: PWA caching for previously loaded content

Accessibility Requirements

REQUIREMENTS (WCAG AA minimum):

Visual

  • High contrast: 4.5:1 for text, 3:1 for UI components
  • Readable typography: Clear, legible fonts with appropriate sizing
  • User-adjustable text size: Small/medium/large controls (see User Preferences)
  • User-adjustable line spacing: Tight/normal/loose controls
  • User-adjustable content width: Narrow/medium/wide controls
  • Color independence: Information not conveyed by color alone
  • Focus indicators: Clear, visible for all interactive elements
  • Responsive text: Scales up to 200% without horizontal scrolling (browser zoom)

Semantic HTML & ARIA

  • Semantic HTML5 elements (<nav>, <main>, <article>, etc.)
  • All interactive elements have aria-label or aria-labelledby
  • Appropriate ARIA roles for custom components
  • ARIA live regions for dynamic content updates
  • All form inputs have associated <label> or aria-labelledby
  • All images have descriptive alt (decorative use empty alt="")
  • Proper heading hierarchy (h1 → h2 → h3, no skipping)

Keyboard Navigation

  • Full keyboard access: All interactive elements accessible via keyboard
  • Logical tab order: Follows visual flow and content structure
  • Skip links: "Skip to main content" for screen reader users
  • Keyboard shortcuts: j/k navigation, r reply, z zap, etc.
  • Focus management: Properly managed in modals, dropdowns, dynamic content
  • No keyboard traps: Users can navigate away from all elements

Screen Reader Support

  • Descriptive text or ARIA labels for all UI elements
  • Status announcements: Loading, errors, success messages
  • ARIA landmarks: navigation, main, complementary, contentinfo
  • Form validation: Errors associated with inputs and announced
  • Dynamic content: Changes announced via ARIA live regions

Additional

  • Reduced motion: Respect prefers-reduced-motion media query
  • High contrast mode: Support Windows High Contrast mode
  • Text alternatives: For emoji and icons where meaning important
  • Link context: Descriptive text (avoid "click here", "read more")
  • Video autoplay: MUST NOT autoplay (require user interaction)
  • Error handling: Clear, actionable error messages
  • Loading states: Clear indication of loading and progress
  • Form instructions: Clear instructions and help text
  • User preferences: Text size, line spacing, content width controls (see User Preferences section)

Input Validation

  • Validate bech32 strings (NIP-19)
  • Validate event signatures
  • Sanitize all HTML content (DOMPurify)

Content Security

  • Sanitize markdown output
  • Validate NIP-21 URIs
  • Escape user content properly

Configuration

Environment Variables

Variable Type Default Validation
VITE_DEFAULT_RELAYS Comma-separated URLs wss://theforest.nostr1.com,wss://nostr21.com,wss://nostr.land,wss://orly-relay.imwald.eu Empty/invalid falls back to defaults
VITE_ZAP_THRESHOLD Integer 1 Must be 0 or positive, invalid defaults to 1
VITE_THREAD_TIMEOUT_DAYS Integer 30 -
VITE_PWA_ENABLED Boolean true -
PORT Integer 9876 Must be 1-65535, invalid defaults to 9876

Deployment

Dockerfile

# Multi-stage build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ARG VITE_DEFAULT_RELAYS
ARG VITE_ZAP_THRESHOLD
ARG VITE_THREAD_TIMEOUT_DAYS
ARG VITE_PWA_ENABLED
ENV VITE_DEFAULT_RELAYS=${VITE_DEFAULT_RELAYS}
ENV VITE_ZAP_THRESHOLD=${VITE_ZAP_THRESHOLD}
ENV VITE_THREAD_TIMEOUT_DAYS=${VITE_THREAD_TIMEOUT_DAYS}
ENV VITE_PWA_ENABLED=${VITE_PWA_ENABLED}
RUN npm run build

FROM httpd:alpine
COPY --from=builder /app/dist /usr/local/apache2/htdocs/
COPY httpd.conf.template /usr/local/apache2/conf/httpd.conf.template
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ARG PORT=9876
ENV PORT=${PORT}
EXPOSE ${PORT}
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

Apache Configuration (httpd.conf.template)

Listen ${PORT}
ServerName localhost

<Directory "/usr/local/apache2/htdocs">
    Options Indexes FollowSymLinks
    AllowOverride All
    Require all granted
</Directory>

<Location "/healthz">
    Header set Content-Type "application/json"
    Header set Cache-Control "public, max-age=5"
</Location>

RewriteEngine On
RewriteBase /
RewriteRule ^healthz$ /healthz.json [L]

RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]

<IfModule mod_headers.c>
    Header set Service-Worker-Allowed "/"
</IfModule>

Entrypoint Script (docker-entrypoint.sh)

#!/bin/sh
set -e

PORT=${PORT:-9876}
if ! [ "$PORT" -ge 1 ] 2>/dev/null || ! [ "$PORT" -le 65535 ] 2>/dev/null; then
  echo "Warning: Invalid PORT '$PORT', using default 9876"
  PORT=9876
fi

envsubst '${PORT}' < /usr/local/apache2/conf/httpd.conf.template > /usr/local/apache2/conf/httpd.conf

exec httpd -D FOREGROUND

Health Check

REQUIREMENTS:

  • Endpoint: /healthz
  • Static JSON file generated at build time
  • Contains: status, service, version, buildTime, gitCommit, timestamp
  • Build script: scripts/generate-healthz.js
  • Vite plugin integration to generate on build