@ -1,963 +1,6 @@
@@ -1,963 +1,6 @@
# aitherboard Specification
# aitherboard
A decentralized messageboard built on the Nostr protocol. This document defines the complete specification for implementation.
A decentralized messageboard built on the Nostr protocol.
This is a client from [silberengel@gitcitadel.com ](https://jumble.imwald.eu/users /npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z )
This is a client from [silberengel@gitcitadel.com ](https://aitherboard.imwald.eu/profile /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` < br > `wss://nostr21.com`< br > `wss://nostr.land`< br > `wss://orly-relay.imwald.eu`< br > `wss://nostr.wine` | Base relays for all operations |
| **Profile Relays** | `wss://relay.damus.io` < br > `wss://aggr.nostr.land`< br > `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 ](#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 <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: ``
- 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
```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`)
```apache
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`)
```bash
#!/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