A decentralized community bulletin board system built on the Nostr protocol. Features NIP-52 calendar events, NIP-28 public chat channels, NIP-17/59 encrypted direct messages, and cohort-based access control. Fully serverless architecture with SvelteKit PWA on GitHub Pages and Google Cloud Platform backend.
| Homepage | Messages | Calendar |
|---|---|---|
![]() |
![]() |
![]() |
| Login | Signup | Mobile View |
![]() |
![]() |
![]() |
| Profile | Semantic Search | Admin Dashboard |
![]() |
![]() |
![]() |
- Public Chat Channels - NIP-28 group messaging with cohort-based access control
- Calendar Events - NIP-52 event scheduling with RSVP support
- Encrypted DMs - NIP-17/59 gift-wrapped private messages
- Semantic Vector Search - AI-powered similarity search with HNSW indexing (100k+ messages)
- PWA Support - Installable app with offline message queue
- Serverless Architecture - Zero infrastructure costs on free tier
- Cohort-Based Access - Business, moomaa-tribe, and admin roles
- Node.js 18+ and npm
- Google Cloud Platform account (free tier)
- GitHub account (for deployment)
# Clone the repository
git clone https://github.com/jjohare/Nostr-BBS.git
cd Nostr-BBS
# Install dependencies
npm install
# Configure environment
cp .env.example .env
# Edit .env with your relay URL and admin pubkey
# Start development server
npm run dev
# Access: http://localhost:5173Frontend (GitHub Pages):
# Build PWA for production
npm run build
# Deployment happens automatically via GitHub Actions on push to main
# Or deploy manually:
npm run deployBackend (Google Cloud Run):
The embedding service is deployed at: https://embedding-api-617806532906.us-central1.run.app
To deploy your own instance, see the Deployment section below for Google Cloud Platform setup instructions.
graph TB
subgraph Internet["Internet Access"]
User["Web Browser"]
CDN["GitHub Pages<br/>(Static CDN)"]
end
subgraph Docker["Docker Container (Internal)"]
Relay["Nostr Relay<br/>(ws://localhost:8008)"]
DB["SQLite/sql.js<br/>(In-memory + File)"]
end
subgraph GCP["Google Cloud Platform"]
CloudRun["Cloud Run<br/>(Embedding API)"]
GCS["Cloud Storage<br/>(Vector Index)"]
end
subgraph Frontend["SvelteKit PWA"]
Static["Static Site<br/>(adapter-static)"]
SW["Service Worker<br/>(Offline Support)"]
IDB["IndexedDB<br/>(Local Cache)"]
end
User -->|HTTPS| CDN
CDN -->|Serves| Static
Static -->|Registers| SW
SW -->|Caches| IDB
Static <-->|WebSocket| Relay
Relay <-->|SQL| DB
Static -->|HTTPS API| CloudRun
CloudRun -->|Vector Index| GCS
IDB -.->|Sync| GCS
style Internet fill:#064e3b,color:#fff
style Docker fill:#2496ed,color:#fff
style GCP fill:#4285f4,color:#fff
style Frontend fill:#1e3a8a,color:#fff
graph LR
subgraph Development["Development"]
Dev["Local Dev<br/>localhost:5173"] -->|npm run dev| Docker["Docker Relay<br/>localhost:8008"]
end
subgraph Production["Production"]
Browser["Browser"] -->|HTTPS| GHP["GitHub Pages<br/>your-username.github.io"]
GHP -->|Loads| PWA["SvelteKit PWA"]
PWA -->|WebSocket| Relay["Docker Relay<br/>(Internal Network)"]
PWA -->|HTTPS| CloudRun["Cloud Run API<br/>embedding-api-*.run.app"]
CloudRun -->|Store| GCS["Google Cloud Storage"]
Relay -->|sql.js| SQLite["SQLite (WASM)"]
end
subgraph CI/CD["GitHub Actions"]
Push["git push main"] -->|Trigger| GHWorkflow["GitHub Actions"]
GHWorkflow -->|Deploy Frontend| GHP
GHWorkflow -->|Build Docker| DockerHub["Docker Image"]
end
style Development fill:#065f46,color:#fff
style Production fill:#1e40af,color:#fff
style CI/CD fill:#6b21a8,color:#fff
graph TB
subgraph Free["Free Tier Services"]
GH["GitHub Pages<br/>âś… Unlimited bandwidth"]
Docker["Docker Hub<br/>âś… Free public images"]
CloudRun["Cloud Run<br/>âś… 2M requests/month<br/>âś… 360,000 GB-seconds"]
GCS["Cloud Storage<br/>âś… 5 GB storage<br/>âś… 5,000 Class A ops<br/>âś… 50,000 Class B ops"]
GHA["GitHub Actions<br/>âś… 2000 min/month"]
end
subgraph Costs["Zero Cost Architecture"]
Static["Static Hosting"] -->|Free| GH
Relay["Nostr Relay"] -->|Free| Docker
API["Embedding API"] -->|Free| CloudRun
VectorIndex["Vector Index"] -->|Free| GCS
Pipeline["CI/CD Pipeline"] -->|Free| GHA
end
style Free fill:#065f46,color:#fff
style Costs fill:#064e3b,color:#fff
Nostr-BBS includes AI-powered semantic search that understands meaning, not just keywords. Search for "schedule tomorrow's meeting" and find messages about "planning the session for Friday" - the system understands context and intent.
graph TB
subgraph Pipeline["Cloud-Based Embedding Pipeline"]
CloudRun["Cloud Run API<br/>REST Endpoints"]
Relay["Nostr Relay<br/>Fetch Messages"]
ST["sentence-transformers<br/>all-MiniLM-L6-v2"]
HNSW["hnswlib<br/>Build Index"]
GCS["Cloud Storage<br/>Store Index"]
end
subgraph PWA["PWA Client"]
WiFi{"WiFi<br/>Detection"}
Sync["Lazy Sync<br/>Background"]
IDB["IndexedDB<br/>Cache Index"]
WASM["hnswlib-wasm<br/>Local Search"]
UI["SemanticSearch<br/>Component"]
end
CloudRun -->|1. Trigger| Relay
Relay -->|2. Kind 1,9| ST
ST -->|3. 384d Vectors| HNSW
HNSW -->|4. Upload| GCS
WiFi -->|5. Check Network| Sync
Sync -->|6. Download| GCS
Sync -->|7. Store| IDB
IDB -->|8. Load| WASM
UI -->|9. Query| WASM
WASM -->|10. Results| UI
style Pipeline fill:#4285f4,color:#fff
style PWA fill:#1e40af,color:#fff
| Feature | Implementation | Benefit |
|---|---|---|
| Semantic Understanding | sentence-transformers/all-MiniLM-L6-v2 | Find by meaning, not just keywords |
| HNSW Index | O(log n) approximate nearest neighbors | Sub-millisecond search on 100k+ vectors |
| Int8 Quantization | 75% storage reduction | 100k messages = ~15MB index |
| WiFi-Only Sync | Network Information API | Respects mobile data caps |
| Offline Search | IndexedDB + hnswlib-wasm | Works without connectivity |
| Nightly Updates | GitHub Actions cron | Always fresh index |
sequenceDiagram
participant API as Cloud Run API
participant Relay as Nostr Relay
participant GCS as Cloud Storage
participant PWA as Browser PWA
participant IDB as IndexedDB
participant WASM as hnswlib-wasm
Note over API,GCS: On-Demand Pipeline (API Triggered)
API->>Relay: 1. Fetch kind 1 & 9 events
Relay-->>API: 2. Return messages
API->>API: 3. Generate embeddings (384d)
API->>API: 4. Quantize to int8
API->>API: 5. Build HNSW index
API->>GCS: 6. Upload index + manifest
Note over PWA,WASM: User Opens App
PWA->>PWA: 7. Check WiFi connection
PWA->>GCS: 8. Fetch manifest.json
GCS-->>PWA: 9. Return version info
alt New Version Available
PWA->>GCS: 10. Download index.bin
GCS-->>PWA: 11. Return ~15MB index
PWA->>IDB: 12. Store in embeddings table
end
Note over PWA,WASM: User Searches
PWA->>IDB: 13. Load index
IDB-->>PWA: 14. Return ArrayBuffer
PWA->>WASM: 15. Initialize HNSW
PWA->>WASM: 16. searchKnn(query, k=10)
WASM-->>PWA: 17. Return note IDs + scores
PWA->>Relay: 18. Fetch full notes by ID
Relay-->>PWA: 19. Return decrypted content
Embedding Model:
name: sentence-transformers/all-MiniLM-L6-v2
dimensions: 384
performance: ~30 sec per 1,000 messages
quantization: int8 (75% size reduction)
HNSW Index:
library: hnswlib (Python) + hnswlib-wasm (Browser)
space: cosine similarity
ef_construction: 200
M: 16
ef_search: 50
Storage:
platform: Google Cloud Storage
bucket: Nostr-BBS-nostr-embeddings
structure:
- latest/manifest.json
- latest/index.bin
- latest/index_mapping.json
versioning: Incremental (v1, v2, ...)
Client Sync:
trigger: WiFi or unmetered connection
storage: IndexedDB (embeddings table)
lazy_load: true (background, non-blocking)| Resource | Limit | Usage (100k msgs) | Headroom |
|---|---|---|---|
| Cloud Run | 2M requests/month | ~10k/month | 99.5% free |
| Cloud Storage | 5 GB storage | ~20 MB | 99.6% free |
| GCS Reads | 50k Class B ops/month | ~10k/month | 80% free |
| GCS Egress | 1 GB/month (free tier) | ~500 MB | 50% free |
| Firestore | 50k reads/day | ~1k/day | 98% free |
import { SemanticSearch } from '$lib/semantic';
// In your Svelte component
<SemanticSearch
onSelect={(noteId) => navigateToMessage(noteId)}
placeholder="Search by meaning..."
/>// Programmatic API
import { searchSimilar, syncEmbeddings, isSearchAvailable } from '$lib/semantic';
// Sync index (automatic on WiFi)
await syncEmbeddings();
// Search for similar messages
const results = await searchSimilar('meeting tomorrow', 10, 0.5);
// Returns: [{ noteId: 'abc123', score: 0.89, distance: 0.11 }, ...]- No Content Storage: Only embeddings stored, not message text
- Encrypted Messages Excluded: NIP-17/59 DMs not indexed (v1)
- Local Processing: Search runs entirely in browser via WASM
- User Control: Manual sync button, no automatic uploads
graph TB
subgraph Core["Core Protocol"]
NIP01["NIP-01<br/>Basic Protocol"]
NIP02["NIP-02<br/>Contact List"]
NIP11["NIP-11<br/>Relay Info"]
NIP42["NIP-42<br/>Authentication"]
end
subgraph Messaging["Messaging & Chat"]
NIP28["NIP-28<br/>Public Channels"]
NIP17["NIP-17<br/>Private DMs"]
NIP44["NIP-44<br/>Encryption"]
NIP59["NIP-59<br/>Gift Wrap"]
NIP25["NIP-25<br/>Reactions"]
end
subgraph Features["Advanced Features"]
NIP52["NIP-52<br/>Calendar Events"]
NIP09["NIP-09<br/>Deletion"]
end
NIP01 --> NIP28
NIP01 --> NIP52
NIP42 --> NIP17
NIP44 --> NIP59
NIP59 --> NIP17
NIP01 --> NIP25
NIP01 --> NIP09
style Core fill:#1e40af,color:#fff
style Messaging fill:#7c2d12,color:#fff
style Features fill:#064e3b,color:#fff
| NIP | Name | Status | Description |
|---|---|---|---|
| NIP-01 | Basic Protocol | âś… Complete | Core event format and relay communication |
| NIP-02 | Contact List | âś… Complete | Following list management |
| NIP-09 | Event Deletion | âś… Complete | Message deletion support |
| NIP-11 | Relay Information | âś… Complete | Relay metadata document |
| NIP-17 | Private DMs | âś… Complete | Sealed rumors for private messaging |
| NIP-25 | Reactions | âś… Complete | Message reactions (emoji) |
| NIP-28 | Public Chat | âś… Complete | Group channels with moderation |
| NIP-42 | Authentication | âś… Complete | Relay authentication challenges |
| NIP-44 | Versioned Encryption | âś… Complete | Modern encryption for DMs |
| NIP-52 | Calendar Events | âś… Complete | Event scheduling with RSVP |
| NIP-59 | Gift Wrap | âś… Complete | Metadata protection layer |
| Kind | NIP | Purpose | Documentation |
|---|---|---|---|
| 0 | 01 | User Profile | Metadata (name, avatar, bio) |
| 1 | 01 | Text Note | Channel messages |
| 4 | 04 | Encrypted DM | Legacy DMs (read-only) |
| 5 | 09 | Deletion | Delete own messages |
| 7 | 25 | Reaction | Emoji reactions |
| 40 | 28 | Channel Creation | Create channel |
| 41 | 28 | Channel Metadata | Update channel |
| 42 | 28 | Channel Message | Post to channel |
| 1059 | 59 | Gift Wrap | Wrapped DMs |
| 31923 | 52 | Calendar Event | Date-based events |
| 31925 | 52 | Calendar RSVP | Event responses |
| 9022 | Custom | Section Access | Cohort-based channel access control |
| 9023 | Custom | Calendar-Channel Link | Event-chatroom integration |
graph TB
subgraph Admin["Admin Actions"]
AdminPubkey["Admin Pubkey<br/>(VITE_ADMIN_PUBKEY)"]
Whitelist["Whitelist Management"]
CohortAssign["Cohort Assignment"]
end
subgraph Cohorts["User Cohorts"]
Admin2["admin<br/>Full system access"]
Business["business<br/>Business community"]
MoomaaTribe["moomaa-tribe<br/>Premium members"]
Public["public<br/>Read-only access"]
end
subgraph Access["Channel Access (Kind 9022)"]
Channel1["meditation-circle<br/>cohorts: [admin, moomaa-tribe]"]
Channel2["business-network<br/>cohorts: [admin, business]"]
Channel3["announcements<br/>cohorts: [public]"]
end
subgraph Auth["NIP-42 Authentication"]
Challenge["AUTH Challenge"]
Verify["Signature Verification"]
CohortCheck["Cohort Membership Check"]
end
AdminPubkey -->|Manages| Whitelist
Whitelist -->|Assigns| CohortAssign
CohortAssign --> Admin2
CohortAssign --> Business
CohortAssign --> MoomaaTribe
CohortAssign --> Public
Admin2 -->|Full Access| Channel1
Admin2 -->|Full Access| Channel2
Admin2 -->|Full Access| Channel3
MoomaaTribe -->|Access| Channel1
Business -->|Access| Channel2
Public -->|Read Only| Channel3
Challenge --> Verify
Verify --> CohortCheck
CohortCheck -->|Grant/Deny| Access
style Admin fill:#b91c1c,color:#fff
style Cohorts fill:#7c2d12,color:#fff
style Access fill:#065f46,color:#fff
style Auth fill:#1e40af,color:#fff
sequenceDiagram
participant Admin
participant App as PWA
participant Relay as Nostr Relay
participant Members as Channel Members
Note over Admin,Members: Create Calendar Event with Channel Link
Admin->>App: 1. Create Calendar Event
App->>App: 2. Build Kind 31923 Event
Note over App: Tags: d (identifier)<br/>title, start, end, location<br/>p (cohorts allowed)
Admin->>App: 3. Link to Channel (optional)
App->>App: 4. Create Kind 9023 Event
Note over App: Links calendar event<br/>to chatroom channel
App->>Relay: 5. Publish Kind 31923 + 9023
Relay->>Relay: 6. Verify admin signature
Relay->>Members: 7. Broadcast to cohort members
Note over Admin,Members: Member RSVP Flow
Members->>App: 8. View Event Details
App->>Relay: 9. Fetch linked channel
Relay-->>App: 10. Return channel info
Members->>App: 11. Click "RSVP - Going"
App->>App: 12. Build Kind 31925 Event
Note over App: Tags: a (event ref)<br/>status (accepted/declined)
App->>Relay: 13. Publish RSVP
Relay->>Admin: 14. Notify organizer
Members->>App: 15. Join event chatroom
App->>Relay: 16. Subscribe to channel
Note over App,Relay: Event chatroom inherits<br/>calendar event cohort access
graph LR
subgraph AdminPanel["Admin Panel"]
Login["Login with Mnemonic<br/>(BIP-39)"]
Dashboard["Admin Dashboard"]
end
subgraph Management["User Management"]
Pending["Pending Requests"]
Approve["Approve Users"]
AssignCohort["Assign Cohorts"]
Revoke["Revoke Access"]
end
subgraph Content["Content Management"]
CreateChannel["Create Channels"]
SetAccess["Set Channel Access"]
CreateEvent["Create Calendar Events"]
LinkEvent["Link Events to Channels"]
Moderate["Moderate Messages"]
end
subgraph Monitoring["Monitoring"]
ViewStats["Channel Statistics"]
ActiveUsers["Active Users"]
RSVPList["Event RSVPs"]
end
Login -->|NIP-06 Key Derivation| Dashboard
Dashboard --> Management
Dashboard --> Content
Dashboard --> Monitoring
Pending -->|Review| Approve
Approve -->|Select| AssignCohort
AssignCohort -->|Kind 9022| SetAccess
CreateChannel -->|Kind 40| SetAccess
SetAccess -->|Cohort Tags| CreateEvent
CreateEvent -->|Kind 31923| LinkEvent
LinkEvent -->|Kind 9023| Moderate
style AdminPanel fill:#b91c1c,color:#fff
style Management fill:#1e40af,color:#fff
style Content fill:#065f46,color:#fff
style Monitoring fill:#6b21a8,color:#fff
graph TB
Start([New User]) --> Signup[Create Account]
Signup --> Keys[Generate Keys<br/>BIP-39 Mnemonic]
Keys --> Backup[Backup Recovery Phrase]
Backup --> Auth[Authenticate to Relay]
Auth --> Dashboard{Main Dashboard}
Dashboard --> Channels[Browse Channels]
Dashboard --> DMs[Direct Messages]
Dashboard --> Events[Calendar Events]
Dashboard --> Profile[User Profile]
Channels --> Join[Join Channel]
Join --> Chat[Send Messages]
Chat --> React[React to Messages]
Chat --> Search[Search Messages]
Chat --> Bookmark[Bookmark Messages]
DMs --> NewDM[New Conversation]
NewDM --> SendDM[Send Encrypted DM]
SendDM --> Receive[Receive DMs]
Events --> Browse[Browse Events]
Events --> Create[Create Event]
Create --> RSVP[RSVP to Events]
Profile --> Settings[Edit Profile]
Settings --> Export[Export Data]
Settings --> Mute[Manage Blocked Users]
style Start fill:#065f46,color:#fff
style Dashboard fill:#1e40af,color:#fff
style Channels fill:#7c2d12,color:#fff
style DMs fill:#6b21a8,color:#fff
style Events fill:#b45309,color:#fff
sequenceDiagram
participant User
participant App as PWA
participant Store as Local Storage
participant Relay as Docker Relay
User->>App: 1. Click "Create Account"
App->>App: 2. Generate BIP-39 Mnemonic
App->>User: 3. Display Recovery Phrase
User->>App: 4. Confirm Backup
App->>App: 5. Derive Keys from Mnemonic
App->>Store: 6. Encrypt & Store Private Key
App->>Relay: 7. Connect WebSocket
Relay->>App: 8. AUTH Challenge (NIP-42)
App->>App: 9. Sign Challenge (Kind 22242)
App->>Relay: 10. Send Signed Event
Relay->>App: 11. OK - Authenticated
App->>User: 12. Show Dashboard
Note over User,Relay: Keys never leave the device
Note over Store: Private key encrypted with PIN
sequenceDiagram
participant User
participant App as PWA
participant Cache as IndexedDB
participant Relay as Docker Relay
participant DB as SQLite
participant Others as Other Users
User->>App: 1. Join Channel
App->>Relay: 2. Subscribe (Kind 40-42)
Relay->>DB: 3. Query Channel Events
DB->>Relay: 4. Return Matching Events
Relay->>App: 5. Stream Existing Messages
App->>Cache: 6. Cache Messages Locally
App->>User: 7. Display Channel
User->>App: 8. Type Message
App->>App: 9. Create Kind 42 Event
App->>App: 10. Sign with Private Key
App->>Relay: 11. Publish Event
Relay->>Relay: 12. Validate Signature
Relay->>DB: 13. Store Event
Relay->>Others: 14. Broadcast to Subscribers
Relay->>App: 15. Confirm Receipt
App->>Cache: 16. Update Local Cache
Others->>Others: 17. Display Message
Note over Cache: Offline support via IndexedDB
Note over Relay: NIP-42 auth + cohort check
sequenceDiagram
participant Alice
participant App as PWA
participant Relay as Docker Relay
participant Bob
Alice->>App: 1. Compose Private Message
Note over App: 2. Create Rumor (Kind 14)<br/>Unsigned inner event
Note over App: 3. Seal with NIP-44<br/>Encrypt with shared secret
Note over App: 4. Generate Random Keypair<br/>For sender anonymity
Note over App: 5. Gift Wrap (Kind 1059)<br/>Fuzz timestamp ±2 days
App->>Relay: 6. Publish Gift Wrap
Note over Relay: Relay sees:<br/>- Random pubkey (not Alice)<br/>- Fuzzed timestamp<br/>- Encrypted content<br/>- Only knows recipient
Relay->>Bob: 7. Deliver to Recipient
Note over Bob: 8. Unwrap Gift<br/>Decrypt outer layer
Note over Bob: 9. Unseal<br/>Decrypt inner rumor
Note over Bob: 10. Read Message<br/>See real sender & time
Bob->>App: 11. Display Message
rect rgb(200, 50, 50, 0.1)
Note over Relay: Privacy guarantees:<br/>✅ Sender hidden (random key)<br/>✅ Time hidden (fuzzed)<br/>✅ Content encrypted<br/>❌ Recipient visible (p tag)
end
sequenceDiagram
participant User
participant App as PWA
participant SW as Service Worker
participant Queue as IndexedDB Queue
participant Relay as Docker Relay
Note over App: User goes offline
User->>App: 1. Send Message
App->>App: 2. Detect Offline
App->>Queue: 3. Queue Message
App->>User: 4. Show "Queued" Status
Note over User,Relay: Network restored
SW->>SW: 5. Detect Online
SW->>Queue: 6. Get Queued Messages
Queue->>SW: 7. Return Messages
loop For each queued message
SW->>Relay: 8. Publish Event
Relay->>SW: 9. Confirm Receipt
SW->>Queue: 10. Remove from Queue
end
SW->>App: 11. Sync Complete Notification
App->>User: 12. Update UI
Note over Queue: Background Sync API<br/>Auto-syncs when online
Nostr-BBS-nostr/
├── src/
│ ├── lib/
│ │ ├── components/ # Svelte components
│ │ │ ├── auth/ # Login, signup, profile
│ │ │ ├── chat/ # Channel list, messages
│ │ │ ├── dm/ # Direct messages
│ │ │ ├── events/ # Calendar, booking
│ │ │ ├── admin/ # Admin panel
│ │ │ ├── forum/ # Forum-style features
│ │ │ └── ui/ # Reusable UI components
│ │ ├── nostr/ # Nostr protocol implementation
│ │ │ ├── keys.ts # BIP-39 key generation
│ │ │ ├── encryption.ts # NIP-44 encryption
│ │ │ ├── dm.ts # NIP-17/59 DM functions
│ │ │ ├── channels.ts # NIP-28 channels
│ │ │ ├── reactions.ts # NIP-25 reactions
│ │ │ ├── calendar.ts # NIP-52 events
│ │ │ └── relay.ts # NDK relay manager
│ │ ├── semantic/ # Semantic vector search
│ │ │ ├── embeddings-sync.ts # WiFi-only GCS sync
│ │ │ ├── hnsw-search.ts # WASM vector search
│ │ │ ├── SemanticSearch.svelte # Search UI component
│ │ │ └── index.ts # Module exports
│ │ ├── stores/ # Svelte stores
│ │ │ ├── auth.ts # Authentication state
│ │ │ ├── channels.ts # Channel subscriptions
│ │ │ ├── messages.ts # Message cache
│ │ │ ├── dm.ts # DM conversations
│ │ │ ├── pwa.ts # PWA state
│ │ │ ├── bookmarks.ts # Bookmarked messages
│ │ │ ├── drafts.ts # Message drafts
│ │ │ └── mute.ts # Blocked users
│ │ └── utils/ # Helper functions
│ │ ├── storage.ts # IndexedDB operations
│ │ ├── crypto.ts # Cryptographic utilities
│ │ ├── search.ts # Message search
│ │ └── export.ts # Data export
│ ├── routes/ # SvelteKit routes
│ │ ├── +page.svelte # Landing page
│ │ ├── chat/ # Chat interface
│ │ ├── dm/ # Direct messages
│ │ ├── events/ # Calendar events
│ │ ├── admin/ # Admin dashboard
│ │ └── settings/ # User settings
│ └── service-worker.ts # PWA service worker
├── embedding-service/ # Google Cloud Run service
│ ├── src/
│ │ ├── main.py # FastAPI application
│ │ ├── embeddings.py # Embedding generation
│ │ ├── hnsw_index.py # HNSW index builder
│ │ ├── gcs_client.py # Cloud Storage client
│ │ └── firestore_client.py # Firestore client
│ ├── Dockerfile # Container definition
│ ├── requirements.txt # Python dependencies
│ ├── cloudbuild.yaml # Cloud Build config
│ └── .gcloudignore # GCP ignore patterns
├── .github/
│ └── workflows/
│ ├── deploy-pages.yml # Frontend deployment to GitHub Pages
│ └── deploy-backend.yml # Backend deployment to Cloud Run
├── static/ # Static assets
│ ├── manifest.json # PWA manifest
│ └── icon-*.png # PWA icons
├── tests/ # Test suites
│ ├── unit/ # Unit tests
│ └── e2e/ # E2E tests
├── docs/ # Documentation
├── svelte.config.js # SvelteKit config (adapter-static)
└── package.json # Node dependencies
# .env (local development)
VITE_RELAY_URL=wss://your-nostr-relay.com
VITE_ADMIN_PUBKEY=<hex-pubkey> # Admin public key (64-char hex)
VITE_NDK_DEBUG=false # Enable NDK debug logging
# Semantic Search (Cloud Storage public URL)
VITE_GCS_EMBEDDINGS_URL=https://storage.googleapis.com/Nostr-BBS-nostr-embeddings
# Cloud Run API
VITE_EMBEDDING_API_URL=https://embedding-api-617806532906.us-central1.run.app
# GCP Configuration (for deployment)
GCP_PROJECT_ID=<your-project-id>
GCP_REGION=us-central1
GCS_BUCKET_NAME=Nostr-BBS-nostr-embeddingsRepository Variables (Settings → Secrets and variables → Actions → Variables):
| Variable | Description |
|---|---|
ADMIN_PUBKEY |
Admin public key (64-char hex format) |
Repository Secrets (Settings → Secrets and variables → Actions → Secrets):
| Secret | Description |
|---|---|
GCP_PROJECT_ID |
Google Cloud project ID |
GCP_SA_KEY |
Service account JSON key (for Cloud Build) |
GCS_BUCKET_NAME |
Cloud Storage bucket name |
The deploy workflow uses ${{ vars.ADMIN_PUBKEY }} to inject the admin key at build time.
The embedding service runs on Google Cloud Run. Key configuration:
- API URL:
https://embedding-api-617806532906.us-central1.run.app - Region:
us-central1 - Concurrency: 80 requests per instance
- Memory: 2 GB
- CPU: 2 vCPU
- Auto-scaling: 0 to 10 instances
PWA settings in static/manifest.json:
{
"name": "Nostr-BBS",
"short_name": "Nostr-BBS",
"start_url": "/",
"display": "standalone",
"background_color": "#1a1a1a",
"theme_color": "#3b82f6",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}This application is designed to run entirely on GCP's free tier:
Cloud Run:
- 2 million requests per month
- 360,000 GB-seconds of memory
- 180,000 vCPU-seconds of compute
- Scale to zero (no charges when idle)
Cloud Storage:
- 5 GB storage
- 5,000 Class A operations (writes) per month
- 50,000 Class B operations (reads) per month
- 1 GB network egress per month
Firestore:
- 1 GiB storage
- 50,000 reads per day
- 20,000 writes per day
- 20,000 deletes per day
Expected Usage (100k messages):
- Cloud Run: ~10k requests/month (0.5% of limit)
- Storage: ~20 MB (0.4% of limit)
- Reads: ~1k/day (2% of daily limit)
- Egress: ~500 MB/month (50% of monthly limit)
Cost Estimate: $0/month on free tier for typical usage
The frontend is automatically deployed via GitHub Actions on every push to main:
-
Setup GitHub Pages:
- Go to repository Settings > Pages
- Source: GitHub Actions
- Branch: main
-
GitHub Actions workflow (
.github/workflows/deploy.yml):name: Deploy to GitHub Pages on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 - run: npm ci - run: npm run build - uses: actions/upload-pages-artifact@v2 with: path: build - uses: actions/deploy-pages@v2
-
Manual deployment:
npm run build npm run deploy
The embedding API is deployed at https://embedding-api-617806532906.us-central1.run.app.
For custom deployments:
Prerequisites:
- Google Cloud SDK installed
- GCP project created
- Billing enabled (free tier available)
Deployment Steps:
# Authenticate with GCP
gcloud auth login
gcloud config set project YOUR_PROJECT_ID
# Create Cloud Storage bucket (first time only)
gsutil mb -l us-central1 gs://Nostr-BBS-nostr-embeddings
gsutil iam ch allUsers:objectViewer gs://Nostr-BBS-nostr-embeddings
# Build and deploy to Cloud Run
cd embedding-service/
gcloud builds submit --tag gcr.io/YOUR_PROJECT_ID/embedding-api
gcloud run deploy embedding-api \
--image gcr.io/YOUR_PROJECT_ID/embedding-api \
--platform managed \
--region us-central1 \
--allow-unauthenticated \
--memory 2Gi \
--cpu 2 \
--concurrency 80 \
--min-instances 0 \
--max-instances 10Environment Configuration:
# Set environment variables for Cloud Run
gcloud run services update embedding-api \
--region us-central1 \
--set-env-vars GCS_BUCKET_NAME=Nostr-BBS-nostr-embeddingsSee also:
- docs/DEPLOYMENT.md - Full deployment guide
- Google Cloud Run Documentation
The Cloud Run service provides the following REST endpoints:
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Health check endpoint |
/api/embeddings/generate |
POST | Generate embeddings for messages |
/api/embeddings/index |
POST | Build and upload HNSW index |
/api/embeddings/manifest |
GET | Get current index version |
/api/embeddings/sync |
POST | Trigger full sync pipeline |
# Run all tests
npm test
# Run unit tests
npm test -- unit
# Run E2E tests with Playwright
npm run test:e2e
# Test specific file
npm test src/lib/nostr/dm.test.ts
# Run tests in watch mode
npm test -- --watch- Private keys stored encrypted in localStorage
- BIP-39 mnemonic backup for key recovery
- Keys never transmitted to server or relay
- Optional PIN/passphrase protection
- NIP-44 encryption for all DMs
- Gift wrap hides sender identity from relay
- Timestamp fuzzing prevents timing analysis
- Content encrypted end-to-end
- NIP-42 authentication required for writes
- Cohort-based whitelist (business, moomaa-tribe, admin)
- Event validation and signature verification
- NIP-09 deletion support
- HTTPS for GitHub Pages (automatic)
- HTTPS for Cloud Run (Google-managed certificates)
- Content Security Policy headers
- CORS configuration
- Google Cloud Armor protection (available)
- No server to compromise
- Cloud Run provides isolated container execution
- Automatic HTTPS with Google-managed certificates
- Google Cloud's DDoS protection
- IAM-based access control
- Zero-trust architecture
Our project uses a comprehensive labeling system for issue and PR management:
priority: critical- Security issues, data loss bugs, service outagespriority: high- Major features, significant bugs affecting many userspriority: medium- Regular features, moderate bugspriority: low- Nice-to-have features, minor improvements
type: bug- Something isn't workingtype: feature- New feature requesttype: enhancement- Improvement to existing featuretype: documentation- Documentation improvementstype: refactor- Code refactoringtype: test- Test-related changestype: security- Security-related issues
area: api- Cloud Run API, backend servicesarea: pwa- Progressive Web App, service workerarea: ui/ux- User interface and experiencearea: encryption- NIP-44, NIP-17/59 encryptionarea: channels- NIP-28 public channelsarea: dm- Direct messaging (NIP-17/59)area: calendar- NIP-52 calendar eventsarea: admin- Admin panel and moderationarea: deployment- GitHub Pages, Google Cloud Platformarea: embeddings- Semantic search, vector embeddings
status: needs triage- Needs review and classificationstatus: blocked- Blocked by dependenciesstatus: in progress- Currently being worked onstatus: needs review- Awaiting code reviewstatus: ready to merge- Approved and ready
good first issue- Good for newcomershelp wanted- Extra attention neededbreaking change- Breaking API changesdependencies- Dependency updates
import { connectRelay, publishEvent, subscribe } from '$lib/nostr';
// Connect to relay (local development)
await connectRelay('ws://localhost:8080', privateKey);
// Publish event
const event = new NDKEvent();
event.kind = 1;
event.content = 'Hello Nostr!';
await publishEvent(event);
// Subscribe to events
const sub = subscribe({ kinds: [1], limit: 10 });
sub.on('event', (event) => console.log(event));import { sendDM, receiveDM, createDMFilter } from '$lib/nostr/dm';
// Send encrypted DM
await sendDM('Hello!', recipientPubkey, senderPrivkey, relay);
// Receive and decrypt
const dm = receiveDM(giftWrapEvent, myPrivkey);
console.log(dm.content, dm.senderPubkey);
// Subscribe to DMs
const filter = createDMFilter(myPubkey);import { createChannel, sendChannelMessage } from '$lib/nostr/channels';
// Create channel (admin only)
await createChannel({
name: 'General',
about: 'General discussion',
picture: 'https://example.com/icon.png'
});
// Send message
await sendChannelMessage(channelId, 'Hello channel!');- Deployment Guide - Serverless deployment and configuration
- GCP Architecture - Google Cloud Platform setup
- GCP Deployment - Cloud Run deployment guide
- GitHub Workflows - CI/CD pipeline configuration
- Security Audit - Security analysis and recommendations
- Audit Report - Detailed security findings
- Admin Key Rotation - Key management procedures
- SQL Injection Fix - Database security hardening
- Direct Messages - NIP-17/59 encrypted messaging
- Message Threading - Threaded conversations
- Reactions - NIP-25 emoji reactions
- Search - Semantic & keyword search
- Mute & Block - User blocking system
- Pinned Messages - Pin important messages
- Link Previews - URL preview generation
- Drafts - Message draft persistence
- Export - Data export functionality
- PWA Implementation - Offline support and installation
- Notifications - Push notification system
- Accessibility - WCAG compliance
- Specification - Requirements and specs
- System Architecture - System design details
- Pseudocode - Algorithm design
- Refinement - Implementation refinement
- Completion - Integration and deployment
- Semantic Search Spec - Vector search requirements
- Search Architecture - Embedding pipeline design
- Search Algorithms - HNSW implementation
- Risk Assessment - Integration risks
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Follow the existing code style
- Write tests for new features
- Update documentation as needed
- Use semantic commit messages
- Ensure all tests pass before submitting PR
MIT License - see LICENSE for details.
This project builds upon exceptional open-source work from the Nostr ecosystem and broader web development community.
- Nostr Protocol - The foundation protocol enabling decentralized, censorship-resistant communication
- NDK (Nostr Dev Kit) - Comprehensive Nostr development toolkit by Pablo Fernandez (@pablof7z)
- nostr-tools - Essential Nostr utilities by fiatjaf
- SvelteKit - Application framework by the Svelte team
- DaisyUI - Beautiful component library by Pouya Saadeghi
- TailwindCSS - Utility-first CSS framework
- Dexie.js - IndexedDB wrapper by David Fahlander
- Transformers.js - Machine learning models by Hugging Face
- sentence-transformers - Multilingual sentence embeddings
- all-MiniLM-L6-v2 - Compact 384d embedding model
- hnswlib - Fast approximate nearest neighbor search
- hnswlib-wasm - WASM-based vector similarity search
- Google Cloud Run - Serverless container platform
- Google Cloud Storage - Object storage for vector embeddings
- Google Firestore - NoSQL metadata database
- Google Cloud Build - CI/CD pipeline
- GitHub Pages - Static site hosting
- GitHub Actions - Frontend deployment automation
- Agentic QE Fleet - AI-powered quality engineering agents (31 QE agents, 53 QE skills)
- Claude Code - AI-assisted development by Anthropic
- Claude Flow - Swarm coordination for parallel development
- ruv-swarm - Multi-agent orchestration
Special thanks to the Nostr community for the NIP specifications:
- NIP-01 - Basic Protocol
- NIP-02 - Contact List
- NIP-09 - Event Deletion
- NIP-11 - Relay Information
- NIP-17 - Private DMs
- NIP-25 - Reactions
- NIP-28 - Public Chat
- NIP-42 - Authentication
- NIP-44 - Versioned Encryption
- NIP-52 - Calendar Events
- NIP-59 - Gift Wrap
- John O'Hare (@jjohare) - Project lead
- Claude Opus 4.5 / Claude Sonnet 4.5 - AI development assistance
- Documentation: See docs/ directory
- Issues: GitHub Issues
- Discussions: GitHub Discussions








