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