A minimal Node.js library for encrypted RPC messaging over the Nostr protocol. NostrMQ provides secure, decentralized message passing with proof-of-work spam prevention and relay pool management.
"NostrMQ lets you build applications like LEGO blocks - choose the best provider for each piece while keeping full control of your identity and data."
Most applications force you into all-or-nothing trust relationships. You either trust a provider with everything, or you self-host.
This limits our design space and options we have to deliver complex systems to people who need them.
NostrMQ uses Nostr like a public message queue to decompose apps into separate components, each run by one or more groups, and links them together through remote RPC.
Each building block can have the trust model that best fits the use case. e.g. let someone else run the client, LLMs etc but keep the signing logic on an isolated device in your house :)
Why Use It?
Don't worry, you don't have to :)
The philosophy and ideas behind it are:
1. User composable apps - Users can choose and swap out components of apps as required. No more "take it or leave it" software with tightly coupled trust models.
2. Sharing the load - Complex systems can be hard to self-host. This lets us share the complexity across both self-hosting and service providers - why not both!
3. Risk sharing - Running Freedom Tech often comes with risk for both server runners and users. The idea is to open up the design space for how we design and share these risks and responsibilities. and introduce more opportunity for....
4. Regulatory arbitrage - Regulation is heavily jurisdiction dependent and risk differs depending on who runs what. This allows you to only run the aspects that are fine in your jurisdiction, then allow users or other service providers to fill in the gaps.
For dissidents in authoritarian societies or those dealing with corruption:
- Operational Security: Keep identity management local while outsourcing heavy computation
- Plausible Deniability: "I just run an encrypted database, i dont process anything", "I'm only a filesystem" , "I only sign messages in a secure enclave"...
- Resilience: If one component gets compromised, the others keep running and we can hotswap providers
- Distributed Risk: No single person carries the full legal/physical risk of the entire system
- 🔐 Encrypted Messaging: NIP-04 encrypted direct messages
- ⚡ Proof-of-Work: Optional spam prevention with configurable difficulty
- 🌐 Multi-Relay Support: Automatic relay pool management with failover
- 🔄 Async/Await: Modern Promise-based API with async iteration support
- 🧵 Multi-threaded PoW: Worker thread support for efficient mining
- 📦 TypeScript: Full TypeScript support with comprehensive type definitions
- 🚀 Zero Dependencies: Minimal footprint with only essential dependencies
- 🛡️ Automatic Replay Protection: Built-in tracking prevents duplicate message processing
NostrMQ automatically prevents replay attacks by tracking processed messages using a lightweight local cache. This feature works out-of-the-box with zero configuration required.
- Timestamp Filtering: Tracks the timestamp of the last processed message
- Event ID Tracking: Maintains an in-memory cache of recent event IDs
- Persistent Storage: Uses
.nostrmq/
directory to survive application restarts - Smart Filtering: Automatically filters relay subscriptions to only fetch new messages
- Graceful Fallback: Continues working if file operations fail (memory-only mode)
Based on comprehensive testing with 81% test coverage:
- Processing Rate: 247 events/second
- Memory Overhead: < 5KB for typical usage
- File I/O: Minimal (only when timestamp advances)
- Cache Efficiency: Configurable limits prevent memory bloat
The tracking system works with sensible defaults, but can be customized via environment variables:
# Override defaults if needed
NOSTRMQ_OLDEST_MQ=3600 # Lookback time in seconds (default: 1 hour)
NOSTRMQ_TRACK_LIMIT=100 # Max recent events to track (default: 100)
NOSTRMQ_CACHE_DIR=.nostrmq # Cache directory (default: .nostrmq)
NOSTRMQ_DISABLE_PERSISTENCE=false # Disable file caching (default: false)
.nostrmq/
├── timestamp.json # Last processed message timestamp
└── snapshot.json # Recent event IDs for duplicate detection
Cache directory creation fails:
- System automatically falls back to memory-only mode
- Check file permissions in your project directory
- Verify disk space availability
High memory usage:
- Reduce
NOSTRMQ_TRACK_LIMIT
to track fewer events - Enable
NOSTRMQ_DISABLE_PERSISTENCE=true
for memory-only mode
Missing old messages:
- Increase
NOSTRMQ_OLDEST_MQ
to look further back - Clear cache directory to reset tracking state
npm install nostrmq
Create a .env
file with your configuration:
# Required: Your Nostr private key (64 hex characters)
NOSTRMQ_PRIVKEY=your_private_key_here
# Required: Relay URLs (comma-separated). No fallback; must be set.
# Example:
# NOSTR_RELAYS=wss://relay.damus.io,wss://nos.lol,wss://relay.nostr.band
NOSTR_RELAYS=
# Optional: Proof-of-Work settings
NOSTR_POW_DIFFICULTY=0
NOSTR_POW_THREADS=4
import { send, receive } from "nostrmq";
// Send a message
const eventId = await send({
target: "recipient_pubkey_hex",
payload: { message: "Hello from NostrMQ!" },
});
// Receive messages
const subscription = receive({
onMessage: (payload, sender, rawEvent) => {
console.log("Received:", payload, "from:", sender);
},
});
// Clean up when done
subscription.close();
Send an encrypted message to a recipient.
Parameters:
options.target
(string): Recipient's public key in hex formatoptions.payload
(object): Data to send (must be JSON-serializable)options.response
(string, optional): Response address (defaults to sender's pubkey)options.relays
(string[], optional): Override default relay URLsoptions.pow
(boolean|number, optional): PoW mining configurationoptions.timeoutMs
(number, optional): Connection timeout (default: 2000ms)
Returns: Promise - Event ID of the published message
Example:
const eventId = await send({
target: "02a1b2c3d4e5f6...",
payload: {
type: "greeting",
message: "Hello!",
timestamp: new Date().toISOString(),
},
pow: 8, // 8-bit proof-of-work
});
Subscribe to incoming encrypted messages.
Parameters:
options.onMessage
(function): Callback for incoming messagesoptions.relays
(string[], optional): Override default relay URLsoptions.autoAck
(boolean, optional): Auto-reply acknowledgment (not implemented)
Returns: SubscriptionHandle with close()
method and async iteration support
Example:
// Callback interface
const subscription = receive({
onMessage: async (payload, sender, rawEvent) => {
console.log("Message from", sender, ":", payload);
},
});
// Async iterator interface
for await (const { payload, sender, rawEvent } of subscription) {
console.log("Received:", payload);
if (shouldStop) break;
}
subscription.close();
Mine proof-of-work for an event template.
Parameters:
event
(object): Event template to minebits
(number): Target difficulty in leading zero bitsthreads
(number, optional): Number of worker threads (default: 1)
Returns: Promise - Event template with nonce tag added
Example:
const minedEvent = await mineEventPow(eventTemplate, 12, 4);
console.log(
"Mined with nonce:",
minedEvent.tags.find((t) => t[0] === "nonce")
);
Load configuration from environment variables.
Returns: NostrMQConfig object with validated settings
Example:
const config = loadConfig();
console.log("Using pubkey:", config.pubkey);
console.log("Connected to relays:", config.relays);
import { send, mineEventPow } from "nostrmq";
// Send with automatic PoW mining
const eventId = await send({
target: "recipient_pubkey",
payload: { urgent: true, data: "Important message" },
pow: 12, // 12-bit difficulty
});
// Manual PoW mining
const eventTemplate = {
kind: 30072,
pubkey: "your_pubkey",
content: "encrypted_content",
tags: [],
created_at: Math.floor(Date.now() / 1000),
};
const minedEvent = await mineEventPow(eventTemplate, 8, 4);
const customRelays = ["wss://relay.example.com", "wss://another-relay.com"];
// Send to custom relays
await send({
target: "recipient_pubkey",
payload: { message: "Hello" },
relays: customRelays,
});
// Receive from custom relays
const subscription = receive({
onMessage: (payload, sender) => console.log(payload),
relays: customRelays,
});
// Send structured application data
await send({
target: "recipient_pubkey",
payload: {
type: "user_update",
operation: "profile_change",
data: {
userId: 12345,
name: "Alice Smith",
email: "[email protected]",
preferences: {
notifications: true,
theme: "dark",
},
},
metadata: {
version: "1.0",
source: "user-service",
timestamp: new Date().toISOString(),
},
},
});
try {
const eventId = await send({
target: "invalid_pubkey", // This will throw
payload: { message: "Hello" },
});
} catch (error) {
if (error.message.includes("invalid pubkey")) {
console.error("Invalid recipient public key");
} else if (error.message.includes("relay")) {
console.error("Relay connection failed");
} else {
console.error("Unexpected error:", error.message);
}
}
Variable | Required | Default | Description |
---|---|---|---|
NOSTRMQ_PRIVKEY |
Yes | - | Your Nostr private key (64 hex chars) |
NOSTR_RELAYS |
Yes | - | Comma-separated relay URLs (no fallback; required) |
NOSTR_POW_DIFFICULTY |
No | 0 |
Default PoW difficulty in bits |
NOSTR_POW_THREADS |
No | 4 |
Worker threads for PoW mining |
See the examples/
directory for complete working examples:
basic-usage.js
- Simple send/receive without PoWpow-usage.js
- Proof-of-work mining examplesadvanced-usage.js
- Advanced features and patterns
"NOSTRMQ_PRIVKEY environment variable is required"
- Ensure your
.env
file contains a valid 64-character hex private key - Check that your application is loading environment variables correctly
"Failed to connect to relays"
- Verify relay URLs are accessible and use
wss://
protocol - Try different relays if current ones are offline
- Check your network connection and firewall settings
"PoW mining timeout"
- Reduce difficulty bits for faster mining
- Increase the number of worker threads
- Consider disabling PoW for testing (
pow: false
)
"Invalid pubkey format"
- Ensure recipient public keys are 64-character hex strings
- Verify the pubkey doesn't include prefixes like
npub
(use hex format)
- Use multiple worker threads for PoW mining on multi-core systems
- Keep PoW difficulty reasonable (8-16 bits) for good performance
- Reuse subscription handles instead of creating new ones frequently
- Use connection pooling by keeping relay connections alive
Enable debug logging by setting the environment variable:
DEBUG=nostrmq:* node your-app.js
NostrMQ includes comprehensive TypeScript definitions:
import { send, receive, SendOpts, ReceiveOpts, NostrMQConfig } from "nostrmq";
const sendOptions: SendOpts = {
target: "recipient_pubkey",
payload: { message: "Hello TypeScript!" },
pow: 8,
};
const eventId: string = await send(sendOptions);
- Fork the repository
- Create a feature branch:
git checkout -b feature-name
- Make your changes and add tests
- Run the build:
npm run build
- Submit a pull request
MIT License - see LICENSE file for details.
- nostr-tools - Core Nostr protocol implementation
- Nostr Protocol - Decentralized social protocol
NostrMQ - Secure, decentralized messaging for the modern web.