39 KiB
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
- Project Structure
- Event Types
- Relay Management
- Authentication
- Content Modules
- User Interface
- Performance Requirements
- Accessibility Requirements
- Security Requirements
- Configuration
- 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 - Markdown:
marked+DOMPurifyfor sanitization - Storage: IndexedDB (via
idborlocalforage) - 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
lud16tags 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,replytags) - 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
ttags (topics) - Threads without topics appear under "General"
- 30-day timeout (configurable)
- Render markdown content
- Cover image: Support
imagetag (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)
rtags with relay URLs- Optional
readorwritemarker (if omitted, relay is both) - Inbox relays (marked
reador unmarked): Added to reading operations - Outbox relays (marked
writeor 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)
rtags with relay URLs (typically local network relays)- Optional
readorwritemarker (if omitted, relay is both) - Inbox relays (marked
reador unmarked): Added to reading operations - Outbox relays (marked
writeor 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
ptags - 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
relaytags 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
paytotags for payment address display - Combine with kind 0
lud16tags (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.comwss://nostr21.comwss://nostr.landwss://nostr.winewss://nostr.sovbit.host |
Base relays for all operations |
| Profile Relays | wss://relay.damus.iowss://aggr.nostr.landwss://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 listingThreadCard.svelte- Thread preview card (landing page)ThreadView.svelte- Full thread displayCreateThreadForm.svelte- Thread creation
REQUIREMENTS:
- Max 3
ttags (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
clienttag (NIP-89) on thread cards - subtle, muted color - Render markdown in full thread view
- Cover image: Display image from
imagetag (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 commentCommentThread.svelte- Threaded comment listCommentForm.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
clienttag (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 buttonZapReceipt.svelte- Zap receipt displayZapInvoiceModal.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)Kind1ReactionButtons.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 displayProfileBadge.svelte- Clickable profile badge with activity indicator and user statusPaymentAddresses.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
- Support multiple:
- 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:
Kind1FeedPage.svelte- Main feed pageKind1Post.svelte- Individual kind 1 postKind1Reply.svelte- Kind 1 reply displayZapReceiptReply.svelte- Zap receipt as reply (with ⚡)CreateKind1Form.svelte- Create new kind 1 eventsReplyToKind1Form.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
- Default suggested:
- Client badge: Display client name from
clienttag (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
- Support system preference detection (
- 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
clienttag (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
- Activity indicator (custom feature, separate from kind 30315):
- Content filtering: Hide content with sensitive tags or #NSFW hashtag
- Hide events with
content-warningtag orsensitivetag - Hide events with
#NSFWhashtag in content or tags - Content is completely hidden (not displayed with warning)
- Hide events with
- 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.
- Format:
- Support NIP-94 (file tags): File attachments
- Format:
["file", "<URL>", "<mime-type>", "size <bytes>", ...]
- Format:
- Support NIP-23 image tags: Cover images for threads/articles
- Format:
["image", "<URL>"]
- Format:
- Media URL normalization and deduplication:
- Extract media URLs from:
imetatags (url field)filetags (first element after "file")imagetags (second element)- Markdown image syntax in content:
 - 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:
imagetag →imetatags →filetags → content-extracted URLs
- Extract media URLs from:
- Display requirements:
- Cover images (from
imagetag): 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
filetags: Display as download links with file type icons - Content-extracted media: Display inline with markdown rendering
- Cover images (from
Thread Organization
REQUIREMENTS:
- Threads sorted by
ttags (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.tsandactivity-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-warningtag orsensitivetag - Hide events with
#NSFWhashtag in content or tags - Content is completely hidden (not displayed with warning)
- Hide events with
- 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-labeloraria-labelledby - Appropriate ARIA roles for custom components
- ARIA live regions for dynamic content updates
- All form inputs have associated
<label>oraria-labelledby - All images have descriptive
alt(decorative use emptyalt="") - 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-motionmedia 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://nostr.wine,wss://nostr.sovbit.host |
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
Links
- Nostr Protocol: https://nostr.com
- nostr-tools Library: https://github.com/nbd-wtf/nostr-tools
- NIP-7D (Kind 11 Threads): [Nostr Improvement Proposals]
- NIP-22 (Kind 1111 Comments): [Nostr Improvement Proposals]
- NIP-57 (Zaps): [Nostr Improvement Proposals]
- NIP-A3 (Kind 10133 Payment Targets): [Nostr Improvement Proposals]
- RFC-8905 (payto: URI scheme): https://www.rfc-editor.org/rfc/rfc8905.html