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
- Technology Stack
- Architecture
- Features
- Authentication
- Content Types
- Configuration
- Docker Deployment
- Health Check
- Development
- Implementation Phases
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+DOMPurifyfor sanitization - Storage: IndexedDB (via
idborlocalforage) - 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 initializationrelay-pool.ts- Relay connection managementevent-store.ts- In-memory event cache with deduplicationsubscription-manager.ts- Active subscription trackingauth-handler.ts- NIP-42 AUTH challenge handlingconfig.ts- Centralized configuration with env var validation
2. Authentication Service (src/lib/services/auth/)
nip07-signer.ts- NIP-07 browser extension signernsec-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 generationsession-manager.ts- Session persistence and key managementprofile-fetcher.ts- Kind 0 metadata fetching
3. Cache Service (src/lib/services/cache/)
indexeddb-store.ts- IndexedDB wrapperevent-cache.ts- Event caching and retrievalprofile-cache.ts- Profile metadata cachingsearch-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/decodingevent-validator.ts- Event signature and structure validationsanitizer.ts- HTML/content sanitization
Feature Modules Layer
5. 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
Features:
- Topic organization (max 3
ttags 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 commentCommentThread.svelte- Threaded comment listCommentForm.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 buttonZapReceipt.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 displayProfileBadge.svelte- Clickable profile badgeRecentlyActive.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 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
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:
- User clicks "View feed" → Opens feed page at
/feed - Feed displays kind 1 events from default relays
- User can create new kind 1 events
- User can reply to kind 1 events with kind 1 or zap
- Zap receipts displayed as replies with ⚡ emoji
- User can reply to kind 1 replies with kind 1
- User can reply to zap receipts with zap receipts or kind 1111 comments
Features
Authentication Methods
- NIP-07 Extension: Browser extension (e.g., Alby, nos2x)
- Nsec Login: Direct bech32 nsec or hex private key
- Optional NIP-49 encryption (ncryptsec) with password
- Stored encrypted in localStorage
- NIP-46 Bunker: Remote signer via bunker connection string
bunker://URI scheme- WebSocket connection to remote signer
- 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 badgenostr:nevent/note/naddr...→ Event card (250 char preview) → Event viewer
Thread Organization
- Threads sorted by
ttags (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 1111commentskind 9734zap 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
- Project setup (Vite, Svelte 5, TypeScript, Tailwind)
- Docker configuration
- Basic Nostr client integration (applesauce)
- Relay connection and AUTH handling
Phase 2: Authentication
- NIP-07 extension login
- Anonymous key generation
- Nsec login (with NIP-49)
- NIP-46 bunker login
- Session management
Phase 3: Core Features
- Thread display (kind 11)
- Comment display (kind 1111)
- Zap receipt display (kind 9735)
- Markdown rendering
- NIP-21 link parsing
Phase 4: Interactions
- Thread creation form
- Comment form
- Zap functionality
- Reaction voting
- Publication status
Phase 5: Advanced Features
- User profiles
- Kind 1 feed page
- Recently active users
- Full-text search
- IndexedDB caching
- Real-time updates
- Relay health monitoring
Phase 6: Polish
- Responsive design
- PWA support
- Keyboard shortcuts
- Error handling
- Loading states
- Performance optimization
Security
Key Security Principles
-
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
-
Validate All Inputs
- Validate bech32 strings (NIP-19)
- Validate event signatures
- Sanitize all HTML content (DOMPurify)
-
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
-
Content Security
- Sanitize markdown output
- Validate NIP-21 URIs
- Escape user content properly
License
[Add your license here]
Contributing
[Add contribution guidelines here]
Links
- Nostr Protocol: https://nostr.com
- Applesauce Library: https://github.com/hzrd149/applesauce
- NIP-7D (Kind 11 Threads): [Nostr Improvement Proposals]
- NIP-22 (Kind 1111 Comments): [Nostr Improvement Proposals]
- NIP-57 (Zaps): [Nostr Improvement Proposals]
Note: This is a comprehensive implementation plan. Refer to the codebase for the actual implementation details.