Mesh-native encrypted storage backend built on:
- Nostr relay for metadata and pub/sub
- Blossom-compatible HTTP server for encrypted blobs
- nostr-vpn (WireGuard) mesh networking for NAT traversal
NostrMesh is designed so nodes behind home or office NAT are still reachable by peers on the private mesh.
- What This Project Solves
- Architecture At A Glance
- Key Features
- Repository Layout
- Prerequisites
- Quick Start
- Validation Workflow
- API Reference
- Configuration
- Operations And Scripts
- Multi-Node Notes
- Known Upstream Limitations
- Troubleshooting Quick Hits
- Security Notes
- Additional Documentation
Traditional self-hosted relay/blob stacks break down under NAT and inbound network restrictions.
NostrMesh uses nostr-vpn as the networking substrate. Every node gets a stable tunnel IP (10.44.x.y) on the WireGuard mesh, exposing relay and blossom endpoints across NATs:
- Relay:
ws://10.44.x.y:8008 - Blossom:
http://10.44.x.y:3000
The API layer binds both systems into a practical upload, resolve, and download flow while preserving encrypted-at-rest semantics.
| Service | Role | Port |
|---|---|---|
nvpn |
Mesh identity and NAT traversal (WireGuard via nostr-vpn) | tunnel host |
db |
Relay persistence (PostgreSQL) | internal (5432) |
cache |
Relay cache/rate infra (Redis) | internal (6379) |
migrate |
Relay schema migrations | one-shot job |
relay |
Nostr relay runtime (nostream-share) |
8008 |
blossom |
Blob store + auth verification | 3000 |
api |
Upload/resolve/download integration API | 4000 |
- Metadata plane: Nostr events (
kind 34578, replaceable byd=<sha256>) - Blob plane: Blossom content-addressed objects (
sha256) - Auth plane: Blossom auth events (
kind 24242, signed, short expiration)
graph TB
subgraph mesh["nostr-vpn Private Mesh (WireGuard tunnels)"]
subgraph nodeA["Node A — 10.44.12.3"]
NVPN_A["nvpn sidecar<br/>(host network, TUN)"]
subgraph stackA["docker-compose"]
RA["Relay :8008"]
BA["Blossom :3000"]
AA["API :4000"]
DBA["PostgreSQL"]
end
NVPN_A ---|":8008"| RA
NVPN_A ---|":3000"| BA
RA --- DBA
AA -->|"WS"| RA
AA -->|"HTTP"| BA
end
subgraph nodeB["Node B — 10.44.7.9"]
NVPN_B["nvpn sidecar<br/>(host network, TUN)"]
subgraph stackB["docker-compose"]
RB["Relay :8008"]
BB["Blossom :3000"]
DBB["PostgreSQL"]
end
NVPN_B ---|":8008"| RB
NVPN_B ---|":3000"| BB
RB --- DBB
end
NVPN_A ===|"WireGuard P2P tunnel<br/>NAT-pierced via STUN"| NVPN_B
DBA -.-|"FDW over 10.44.x.y:5432"| DBB
end
- Client uploads file to API.
- API encrypts file payload and uploads ciphertext to Blossom.
- API emits encrypted metadata event to relay.
- Client resolves metadata by hash or event ID.
- API fetches encrypted blob, decrypts, and returns original payload.
- Mesh-native endpoint generation (
BLOSSOM_PUBLIC_URLandRELAY_PUBLIC_URL) - Encrypted blobs (AES-256-GCM) with per-file keys
- Encrypted metadata payloads (
aead-v1envelope) - Soft delete via replaceable metadata events (
deleted: true) - Structured API error contract (
error,code, optionaldetails) - Retry and backoff for relay/blossom operations
- Upload idempotency with
Idempotency-Keyand conflict detection - Health, smoke, mesh, integration, and demo scripts for reproducibility
api/ API service (TypeScript)
blossom/ Blossom-compatible server
docs/ Architecture, runbook, dependency contract
nostream-share/ Embedded relay runtime source
scripts/ Stack lifecycle, diagnostics, demos
tests/ Smoke and integration validations
docker-compose.yml Full stack orchestration
Required host tools:
- Docker Engine with Compose v2 (
docker compose) - Bash
curljqgit
Host constraints:
- Linux host with
/dev/net/tunfor WireGuard - Permission to run containers with
NET_ADMIN - Ports available:
3000,4000,8008
From repository root:
./scripts/stack-up.shThen verify:
./scripts/health-check.sh
./scripts/mesh-test.shWhat stack-up.sh handles for you:
- Verifies dependencies and compose file.
- Bootstraps relay checkout if missing.
- Generates or updates
.envviascripts/init-env.sh. - Starts all services in
docker-compose.yml. - Discovers mesh IP (WireGuard tunnel IP) and refreshes public URLs.
- Runs health checks and prints local + mesh endpoints.
./tests/smoke/relay-connectivity.sh
./tests/smoke/pubsub.sh./scripts/run-integration-tests.sh./scripts/demo.shThe demo includes automatic fallback to mesh proof when known upstream relay publish behavior rejects replaceable events.
Base URL: http://127.0.0.1:4000
GET /health
POST /blobs- multipart field:
file - optional multipart field:
folder(must start with/) - optional header:
Idempotency-Key - response:
201witheventId,hash,downloadUrl,metadata
- multipart field:
GET /blobs/:hash- response: metadata + download URL
GET /blobs/:hash/download- response: decrypted binary payload
DELETE /blobs/:hash- response: soft-delete event result (
alreadyDeletedon idempotent replay)
- response: soft-delete event result (
GET /events/:eventId- response: raw event and parsed metadata
GET /events?hash=<sha256>- response: event list by metadata hash
Non-2xx responses return JSON:
{
"error": "human-readable message",
"code": "machine_code",
"details": {}
}Typical status/code patterns:
400: invalid hash/eventId/folder/idempotency key404: metadata or event not found409: idempotency key reused with different payload410: metadata/blob soft-deleted422: invalid persisted metadata event502: relay or blossom upstream failure
Main .env values managed by scripts/init-env.sh:
| Variable | Purpose |
|---|---|
SECRET |
Relay secret |
NOSTR_SECRET_KEY |
API signing + metadata encryption secret |
API_PORT |
API bind port |
RELAY_URL / RELAY_URLS |
Relay endpoints for API read/write |
BLOSSOM_URL |
Internal blossom URL used by API |
BLOSSOM_PUBLIC_URL |
Public/mesh blossom URL stored in metadata |
RELAY_PUBLIC_URL |
Public/mesh relay URL shown in output |
RELAY_PUBLISH_ATTEMPTS |
Relay publish retry count |
RELAY_QUERY_ATTEMPTS |
Relay query retry count |
RELAY_RETRY_BASE_DELAY_MS |
Relay retry backoff base |
BLOSSOM_UPLOAD_ATTEMPTS |
Blossom upload retry count |
BLOSSOM_DOWNLOAD_ATTEMPTS |
Blossom download/head retry count |
BLOSSOM_RETRY_BASE_DELAY_MS |
Blossom retry backoff base |
IDEMPOTENCY_TTL_SECONDS |
Idempotency cache TTL |
IDEMPOTENCY_MAX_ENTRIES |
Idempotency cache capacity |
| Script | Purpose |
|---|---|
scripts/stack-up.sh |
Full start + env init + health report |
scripts/stack-down.sh |
Full shutdown (-v to remove volumes) |
scripts/health-check.sh |
HTTP, container, DB, cache, mesh checks |
scripts/mesh-test.sh |
Mesh connectivity + blob roundtrip proof |
scripts/demo.sh |
User-facing end-to-end walkthrough |
scripts/run-integration-tests.sh |
Integration test orchestration |
scripts/mesh-init.sh |
Initialize nvpn network, generate invite |
scripts/setup-fdw.sh |
Setup DB partition mapping over the tunnel |
scripts/discover-tunnel-ip.sh |
Resolve current mesh tunnel IP |
NostrMesh supports multi-node operation over a private nostr-vpn mesh.
High-level process:
- Start Node A (
./scripts/mesh-init.shthen./scripts/stack-up.sh). Note the invite link and tunnel IP. - Join Node B to the mesh using the invite link from Node A. Start Node B.
- Establish FDW (Foreign Data Wrapper) partitioning across the mesh by running
./scripts/setup-fdw.shon Node A. - Verify cross-node relay and blossom reachability over mesh URLs.
Use the full operational walkthrough in docs/runbook.md.
- Mesh address missing:
- Check
nostrmesh-vpnlogs and/dev/net/tunavailability.
- Check
- Relay metadata publish errors:
- Run
./scripts/health-check.shthen./scripts/mesh-test.sh.
- Run
- Migration drift:
- For disposable local runs, reset with
./scripts/stack-down.sh -vthen restart.
- For disposable local runs, reset with
- Blob access is authenticated with signed
kind 24242events. - Blob payloads are encrypted before persistence.
- Metadata payloads are encrypted before relay publish.
nostr-vpn(WireGuard) provides encrypted transport across mesh nodes.
Local dev note:
- Relay auth is intentionally disabled in generated local relay settings for deterministic smoke workflows.
docs/architecture.mdfor detailed system model and protocol assumptionsdocs/runbook.mdfor operations, backup, and troubleshooting proceduresdocs/dependencies.mdfor version pins and environment contract