You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

23 KiB

Aitherboard

A dockerized 4chan-style imageboard built on Nostr protocol. Aitherboard is a single-page application (SPA) that provides a decentralized discussion board experience with real-time updates, multiple authentication methods, and full Nostr protocol integration.

Table of Contents

Overview

Aitherboard is a decentralized imageboard that uses the Nostr protocol for content distribution. It supports:

  • Kind 11 threads (NIP-7D) - Discussion threads with topics
  • Kind 1111 comments (NIP-22) - Threaded comments
  • Kind 9735 zap receipts (NIP-57) - Lightning payments displayed as comments
  • Kind 1 feed - Twitter-like feed with replies and zaps
  • Kind 7 reactions - Upvote/downvote system
  • Real-time updates - Live feed of new content
  • Multiple auth methods - NIP-07, nsec, NIP-46 bunker, anonymous
  • Full markdown rendering - All content supports markdown
  • NIP-21 link parsing - Clickable profile badges and event cards

Technology Stack

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

Architecture

Architecture Layers

┌─────────────────────────────────────────────────────────┐
│                    UI Components Layer                    │
│  (LandingPage, ThreadList, ThreadView, Profile, Feed)   │
└───────────────────────┬─────────────────────────────────┘
                        │
┌───────────────────────▼─────────────────────────────────┐
│                  Feature Modules Layer                   │
│  (Threads, Comments, Zaps, Reactions, Profiles, Feed)    │
└───────────────────────┬─────────────────────────────────┘
                        │
┌───────────────────────▼─────────────────────────────────┐
│                  Core Services Layer                     │
│  (Nostr Client, Relay Pool, Auth, Cache, Security)      │
└───────────────────────┬─────────────────────────────────┘
                        │
┌───────────────────────▼─────────────────────────────────┐
│                  Applesauce Library                      │
│              (Nostr Protocol Implementation)             │
└─────────────────────────────────────────────────────────┘

Core Services Layer

1. Nostr Client Service (src/lib/services/nostr/)

  • applesauce-client.ts - Applesauce client initialization
  • relay-pool.ts - Relay connection management
  • event-store.ts - In-memory event cache with deduplication
  • subscription-manager.ts - Active subscription tracking
  • auth-handler.ts - NIP-42 AUTH challenge handling
  • config.ts - Centralized configuration with env var validation

2. Authentication Service (src/lib/services/auth/)

  • nip07-signer.ts - NIP-07 browser extension signer
  • nsec-signer.ts - Direct nsec/hex key signer (with NIP-49 encryption)
  • bunker-signer.ts - NIP-46 remote signer (bunker)
  • anonymous-signer.ts - Anonymous session key generation
  • session-manager.ts - Session persistence and key management
  • profile-fetcher.ts - Kind 0 metadata fetching

3. Cache Service (src/lib/services/cache/)

  • indexeddb-store.ts - IndexedDB wrapper
  • event-cache.ts - Event caching and retrieval
  • profile-cache.ts - Profile metadata caching
  • search-index.ts - Full-text search index

4. Security Utilities (src/lib/services/security/)

  • key-management.ts - Key encryption/decryption (NIP-49)
  • bech32-utils.ts - NIP-19 bech32 encoding/decoding
  • event-validator.ts - Event signature and structure validation
  • sanitizer.ts - HTML/content sanitization

Feature Modules Layer

5. 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

Features:

  • Topic organization (max 3 t tags per thread)
  • "General" category for threads without topics
  • 30-day timeout (configurable)
  • Thread statistics (comments, zaps, activity)
  • Sorting: newest, most active, most upvoted
  • Plaintext preview extraction for landing page (no markdown/images)

Landing Page Thread Display:

  • Title
  • Relative time of thread creation
  • Relative time of latest response
  • Profile badge
  • Vote stats
  • First 250 chars of plaintext (no markdown/images)

6. Comment Module (src/lib/modules/comments/)

Components:

  • Comment.svelte - Individual comment
  • CommentThread.svelte - Threaded comment list
  • CommentForm.svelte - Reply form

Threading Style:

  • 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

7. Zap Module (src/lib/modules/zaps/)

Components:

  • ZapButton.svelte - Zap action button
  • ZapReceipt.svelte - Zap receipt display

Features:

  • Create kind 9734 zap requests
  • Send to recipient's lnurl callback (wallet)
  • Display kind 9735 zap receipts (≥ configured threshold)
  • Zap receipts displayed with emoji
  • Configurable minimum sat threshold (default: 1, must be 0 or positive)

Important: Zap requests (9734) go to wallets, NOT relays. Only receipts (9735) are published to relays.

8. Reaction Module (src/lib/modules/reactions/)

Components:

  • ReactionButtons.svelte - Upvote/downvote buttons

Rules:

  • Only + and - content allowed
  • One vote per user per event
  • Clicking same button twice deletes the reaction

9. Profile Module (src/lib/modules/profiles/)

Components:

  • ProfilePage.svelte - User profile display
  • ProfileBadge.svelte - Clickable profile badge
  • RecentlyActive.svelte - Recently active users list

Features:

  • Display kind 0 metadata (name, about, picture, NIP-05)
  • Show kind 1 feed from default relays + wss://relay.damus.io + wss://aggr.nostr.land
  • Allow zapping profile owner
  • Allow replying to kind 1 notes with kind 1 replies (NIP-10) or zaps
  • Recently active users tracking (15min/1hr/24hr windows)

10. Kind 1 Feed Module (src/lib/modules/feed/)

Components:

  • Kind1FeedPage.svelte - Main feed page
  • Kind1Post.svelte - Individual kind 1 post
  • Kind1Reply.svelte - Kind 1 reply display
  • ZapReceiptReply.svelte - Zap receipt as reply (with )
  • CreateKind1Form.svelte - Create new kind 1 events
  • ReplyToKind1Form.svelte - Reply to kind 1 events

Features:

  • "View feed" button on landing page opens feed page
  • Fetch kind 1 events from default relays
  • Create new kind 1 events with markdown editor
  • Reply to kind 1 with kind 1 (NIP-10 threading)
  • Reply to kind 1 with zap (display receipt as reply with emoji)
  • Reply to kind 1 replies with their own kind 1
  • Reply to zap receipts with:
    • Zap receipts (zap the zapper)
    • Kind 1111 comments
  • All content rendered as markdown
  • Flat threading display (no indentation, similar to comment threading)
  • Real-time updates
  • Infinite scroll

Interaction Flow:

  1. User clicks "View feed" → Opens feed page at /feed
  2. Feed displays kind 1 events from default relays
  3. User can create new kind 1 events
  4. User can reply to kind 1 events with kind 1 or zap
  5. Zap receipts displayed as replies with emoji
  6. User can reply to kind 1 replies with kind 1
  7. User can reply to zap receipts with zap receipts or kind 1111 comments

Features

Authentication Methods

  1. NIP-07 Extension: Browser extension (e.g., Alby, nos2x)
  2. Nsec Login: Direct bech32 nsec or hex private key
    • Optional NIP-49 encryption (ncryptsec) with password
    • Stored encrypted in localStorage
  3. NIP-46 Bunker: Remote signer via bunker connection string
    • bunker:// URI scheme
    • WebSocket connection to remote signer
  4. Anonymous: Temporary nsec for session
    • Generated on first visit
    • Pattern-based avatar
    • Handle: Aitherite{random}
    • Only allows zap requests (no comments)

Content Display

  • Threads (Kind 11): Discussion threads with topics
  • Comments (Kind 1111): Threaded comments with NIP-22 tags
  • Zap Receipts (Kind 9735): Lightning payments displayed as comments with
  • Kind 1 Feed: Twitter-like feed with replies and zaps
  • Reactions (Kind 7): Upvote/downvote system
  • Markdown Rendering: All content supports full markdown
  • NIP-21 Links:
    • nostr:npub... → Clickable profile badge
    • nostr:nevent/note/naddr... → Event card (250 char preview) → Event viewer

Thread Organization

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

Thread Creation

  • Form to create new threads
  • Topic selection (multi-select, max 3)
  • Suggested topics (same as Discussion creation form in jumble)
  • Publish to:
    • Default relays
    • wss://thecitadel.nostr1.com
    • User's kind 10002 outbox relays
  • Display all target relays with deselection option
  • Publication status modal:
    • Success/failure per relay
    • Error messages (hyperlink https:// URLs)
    • Auto-close after 30s or manual close

Commenting and Zapping

  • Response-threading: Flat list with gray blurb showing parent
  • Click gray blurb to highlight/scroll to parent
  • Respond to comments or zap receipts with:
    • kind 1111 comments
    • kind 9734 zap requests
  • Zap requests sent to wallets, receipts displayed from relays

Reactions

  • Upvote/downvote buttons (kind 7)
  • One vote per user per event
  • Clicking twice deletes the vote
  • Display vote counts

Advanced Features

  • Full-text search across threads/comments (keyboard shortcut /)
  • Local caching (IndexedDB) for offline access
  • Thread statistics (comment counts, zap totals, activity)
  • Real-time updates (live indicators for new comments)
  • Relay health monitoring (connection status, latency, auto-retry)
  • User blocking/muting
  • Content warnings (NSFW/sensitive content)
  • Thread locking (prevent replies to old/closed threads)
  • Keyboard shortcuts (j/k, r, z, etc.)
  • Thread bumping (active threads rise to top)
  • Recently active users list
  • User profile pages with kind 1 feeds

Configuration

Environment Variables

All configuration via Docker environment variables:

# Default relays (comma-separated, overrides hardcoded defaults)
VITE_DEFAULT_RELAYS=wss://theforest.nostr1.com,wss://nostr21.com,wss://nostr.land,wss://nostr.wine,wss://nostr.sovbit.host

# Minimum zap threshold in sats (must be 0 or positive integer, default: 1)
VITE_ZAP_THRESHOLD=1

# Server port (overrides default 9876, must be 1-65535)
PORT=9876

# Other configuration
VITE_THREAD_TIMEOUT_DAYS=30
VITE_PWA_ENABLED=true

Validation:

  • VITE_ZAP_THRESHOLD: Must be 0 or positive integer. Invalid/negative values default to 1.
  • VITE_DEFAULT_RELAYS: Comma-separated list of relay URLs. Empty/invalid falls back to hardcoded defaults.
  • PORT: Must be valid port number (1-65535). Invalid values default to 9876.

Default Relays

wss://theforest.nostr1.com
wss://nostr21.com
wss://nostr.land
wss://nostr.wine
wss://nostr.sovbit.host

Profile Relays (additional)

wss://relay.damus.io
wss://aggr.nostr.land

Docker Deployment

Dockerfile

# Multi-stage build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Build with environment variables (build-time)
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
# Port is configurable via PORT env var (runtime)
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>

# Health check endpoint - must be before SPA routing
<Location "/healthz">
    Header set Content-Type "application/json"
    Header set Cache-Control "public, max-age=5"
</Location>

# Rewrite /healthz to /healthz.json (must be before SPA catch-all rule)
RewriteEngine On
RewriteBase /
RewriteRule ^healthz$ /healthz.json [L]

# SPA routing (after healthz)
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]

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

Entrypoint Script (docker-entrypoint.sh)

#!/bin/sh
set -e

# Validate and set PORT (must be 1-65535)
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

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

# Start Apache
exec httpd -D FOREGROUND

Docker Compose

services:
  aitherboard:
    build:
      context: .
      args:
        VITE_DEFAULT_RELAYS: ${VITE_DEFAULT_RELAYS:-wss://theforest.nostr1.com,wss://nostr21.com,wss://nostr.land,wss://nostr.wine,wss://nostr.sovbit.host}
        VITE_ZAP_THRESHOLD: ${VITE_ZAP_THRESHOLD:-1}
        VITE_THREAD_TIMEOUT_DAYS: ${VITE_THREAD_TIMEOUT_DAYS:-30}
        VITE_PWA_ENABLED: ${VITE_PWA_ENABLED:-true}
    ports:
      - "${PORT:-9876}:${PORT:-9876}"
    environment:
      - PORT=${PORT:-9876}

Usage

# Build with custom relays
docker build --build-arg VITE_DEFAULT_RELAYS="wss://relay1.com,wss://relay2.com" .

# Run with custom port
docker run -e PORT=8080 -p 8080:8080 aitherboard

# Run with custom zap threshold (must be 0 or positive)
docker run -e VITE_ZAP_THRESHOLD=5 aitherboard

# Using docker-compose
docker-compose up -d

Health Check

/healthz Endpoint

A health check endpoint for monitoring systems (Kubernetes, Docker healthchecks, load balancers, etc.).

Implementation: Static JSON file generated at build time.

Build Script (scripts/generate-healthz.js):

import { writeFileSync } from 'fs'
import { readFileSync } from 'fs'
import { execSync } from 'child_process'
import { fileURLToPath } from 'url'
import { dirname, join } from 'path'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'))
const buildTime = new Date().toISOString()

let gitCommit = 'unknown'
try {
  gitCommit = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim()
} catch (e) {
  // Git not available or not a git repo
}

const healthz = {
  status: 'ok',
  service: 'aitherboard',
  version: packageJson.version || '0.0.0',
  buildTime,
  gitCommit,
  timestamp: new Date().toISOString()
}

writeFileSync(
  join(__dirname, '../public/healthz.json'),
  JSON.stringify(healthz, null, 2)
)

Vite Integration:

import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import generateHealthz from './scripts/generate-healthz.js'

export default defineConfig({
  plugins: [
    svelte(),
    {
      name: 'generate-healthz',
      buildStart() {
        generateHealthz()
      }
    }
  ]
})

Response Example:

{
  "status": "ok",
  "service": "aitherboard",
  "version": "1.0.0",
  "buildTime": "2024-01-15T10:30:00.000Z",
  "gitCommit": "a1b2c3d",
  "timestamp": "2024-01-15T12:45:30.123Z"
}

Monitoring Integration:

  • Kubernetes: http://service:9876/healthz
  • Docker: HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:9876/healthz || exit 1
  • Load balancers, Prometheus, etc.

Development

Project Structure

aitherboard/
├── src/
│   ├── lib/
│   │   ├── services/        # Core services
│   │   │   ├── nostr/      # Nostr client, relay pool
│   │   │   ├── auth/       # Authentication
│   │   │   ├── cache/      # IndexedDB caching
│   │   │   └── security/   # Security utilities
│   │   ├── modules/        # Feature modules
│   │   │   ├── threads/    # Thread management
│   │   │   ├── comments/   # Comment threading
│   │   │   ├── zaps/       # Zap handling
│   │   │   ├── reactions/  # Upvote/downvote
│   │   │   ├── profiles/   # User profiles
│   │   │   └── feed/       # Kind 1 feed
│   │   └── components/     # UI components
│   │       ├── content/    # Markdown, NIP-21 links
│   │       ├── interactions/# Buttons, forms
│   │       ├── layout/     # Pages, navigation
│   │       └── modals/     # Dialogs, overlays
│   └── routes/             # SvelteKit routes
├── public/                 # Static assets
├── scripts/                # Build scripts
├── Dockerfile
├── docker-compose.yml
├── httpd.conf.template
└── docker-entrypoint.sh

Key Nostr Event Structures

Kind 11 (Thread)

{
  kind: 11,
  content: string, // Markdown content
  tags: [
    ['title', string],
    ['t', topic1],
    ['t', topic2], // max 3 topics
    ...
  ]
}

Kind 1111 (Comment)

{
  kind: 1111,
  content: string, // Markdown content
  tags: [
    ['K', '11'], // Root thread kind
    ['E', rootEventId], // Root thread event
    ['e', parentEventId], // Direct parent (if replying)
    ['k', '1111'], // Parent kind
    ['p', authorPubkey], // Author being replied to
    ['P', rootAuthorPubkey], // Root thread author
    ...
  ]
}

Kind 9734 (Zap Request)

{
  kind: 9734,
  content: string, // Zap message
  tags: [
    ['p', recipientPubkey],
    ['e', eventId], // Optional: event being zapped
    ['relays', ...relayUrls],
    ['amount', millisats],
    ...
  ]
}

Kind 9735 (Zap Receipt)

{
  kind: 9735,
  content: string, // Bolt11 invoice
  tags: [
    ['p', recipientPubkey],
    ['e', eventId], // Optional: event being zapped
    ['bolt11', invoice],
    ['description', zapRequestJson],
    ['preimage', preimage], // Optional
    ...
  ]
}

Kind 7 (Reaction)

{
  kind: 7,
  content: '+' | '-', // Only + or - allowed
  tags: [
    ['e', eventId], // Event being reacted to
    ...
  ]
}

Kind 1 (Note/Feed Post)

{
  kind: 1,
  content: string, // Markdown content
  tags: [
    ['e', eventId], // Optional: event being replied to
    ['p', pubkey], // Optional: pubkey being replied to
    ['root', rootEventId], // Optional: root of thread
    ['reply', replyEventId], // Optional: direct parent
    ...
  ]
}

Implementation Phases

Phase 1: Foundation

  1. Project setup (Vite, Svelte 5, TypeScript, Tailwind)
  2. Docker configuration
  3. Basic Nostr client integration (applesauce)
  4. Relay connection and AUTH handling

Phase 2: Authentication

  1. NIP-07 extension login
  2. Anonymous key generation
  3. Nsec login (with NIP-49)
  4. NIP-46 bunker login
  5. Session management

Phase 3: Core Features

  1. Thread display (kind 11)
  2. Comment display (kind 1111)
  3. Zap receipt display (kind 9735)
  4. Markdown rendering
  5. NIP-21 link parsing

Phase 4: Interactions

  1. Thread creation form
  2. Comment form
  3. Zap functionality
  4. Reaction voting
  5. Publication status

Phase 5: Advanced Features

  1. User profiles
  2. Kind 1 feed page
  3. Recently active users
  4. Full-text search
  5. IndexedDB caching
  6. Real-time updates
  7. Relay health monitoring

Phase 6: Polish

  1. Responsive design
  2. PWA support
  3. Keyboard shortcuts
  4. Error handling
  5. Loading states
  6. Performance optimization

Security

Key Security Principles

  1. Never Store Plaintext Keys

    • Use NIP-49 encryption for stored keys
    • Store encrypted key in localStorage (never plaintext)
    • Anonymous keys: Generate fresh on session start, discard on close
  2. Validate All Inputs

    • Validate bech32 strings (NIP-19)
    • Validate event signatures
    • Sanitize all HTML content (DOMPurify)
  3. Secure Key Handling

    • NIP-07: Use browser extension (no key storage)
    • Nsec: Encrypt with NIP-49 before storage
    • Bunker: No local key storage
    • Anonymous: Session-only, never persisted
  4. Content Security

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

License

[Add your license here]

Contributing

[Add contribution guidelines here]


Note: This is a comprehensive implementation plan. Refer to the codebase for the actual implementation details.