Nosterm documentation
Nostr with an IRC soul.
What is Nosterm?
Nosterm is a browser-based Nostr chat client that looks and behaves like a classic IRC command-line terminal. Old-school chat, new-world protocol: you get the familiar feel of channels and slash commands, running on an open, decentralized network instead of someone else's server.
There are no accounts and no passwords — your identity is a cryptographic key pair that
you control. You join channels, chat in real time, send encrypted direct messages, and
moderate rooms, all by typing commands the way you would in irssi or
WeeChat.
How it works
Nosterm is a client-only single-page app. There is no Nosterm backend sitting between you and the network — the client connects directly to Nostr relays over WebSockets and speaks the protocol itself.
- Relays are servers that store and forward messages. You can connect to any relay, several at once, or run your own.
- Rooms are NIP-29 managed groups — the equivalent of IRC channels, scoped to the relay that hosts them.
-
Your identity is a Nostr key pair. Your public key (
npub) is who you are; your private key (nsec) signs everything you send.
Everything you see arrives as signed events from relays and is rendered as plain text — never as HTML — so remote content can't forge system messages or inject styles.
Getting started
- Open the client and pick a way to sign in (or explore with a temporary identity).
- You're connected to a default relay automatically. Type
/helpto see every command. - Join a channel:
/join #general. - Say hello — just type and press Enter. Text without a leading
/is a message to the active room. - Set a display name others will see:
/nick yourname.
Tab to autocomplete commands, and
/help <command> for usage on any single command.
Signing in
No account. No password. Just your keys. Nosterm never asks you to register — you bring a Nostr identity, or generate one on the spot. Choose the method that fits how much you trust the browser:
Browser extension (NIP-07) — recommended
A signer extension (such as Alby or nos2x) holds your private key and signs on request.
Your key never enters the page. Run /login nip07 or click
Browser extension on the sign-in screen.
Remote signer (bunker / NIP-46)
Your key stays on a remote signer and approves each action over a connection string.
Use /login bunker and paste a bunker://… URL or a NIP-05
address.
Create a new key
Generate a fresh identity in seconds. Nosterm shows your nsec
once — save it somewhere safe. Optionally remember it on this device,
encrypted with a passphrase.
Paste an nsec (advanced)
nsec puts your private key
directly in the browser tab. Prefer an extension or remote signer whenever possible.
Explore with a temporary identity
Not ready to commit? Start with an ephemeral key to look around. It's discarded when you leave.
Rooms & relays
A room is a NIP-29 managed group — relay-scoped, with a real roster,
invites, and moderation. Because rooms belong to a relay, #general on one
relay is a different room than #general on another.
/join #general # on the active relay
/join wss://relay.example/#general # explicit relay + group
/rooms # list rooms you've joined
/switch #general # change the active room
/part # leave the active room
Manage relay connections directly:
/connect wss://relay.example # connect to a relay
/relays # list connected relays
/relays discover # connect your NIP-65 relay list
/disconnect wss://relay.example # disconnect
Room admins can invite and remove members with /invite <npub> and
/kick <npub>.
Federation
A channel doesn't have to live on a single relay. With federation, a home relay can mirror or ingest chat with peer relays run by other operators, so a room survives any one relay going down and can be spread across the network — channels without a single point of failure.
Federation is deliberately narrow, which is what keeps it safe:
- Chat-only. Only chat messages (NIP-29 kind 9/10) cross the relay boundary. Membership, admins, bans, and metadata stay local to each relay — every operator moderates their own instance, so there are no cross-relay moderation conflicts to resolve.
-
Mirror or ingest. A
mirrorpeer is bidirectional — chat posted on either relay is forwarded to the other. Aningestpeer is read-only: you pull chat from it but never push back (handy for an announcement feed). - Per-channel scope. Federate every channel, or just a named few.
-
Forgery-resistant. Incoming events are re-verified before they're
accepted — a peer can't inject messages signed by someone else — and duplicate events
are dropped, so
A↔B↔Aloops die on second sight.
Operators turn it on with a single environment variable listing peers, their mode, and the channels to share:
# Mirror two channels with peer A; ingest everything from peer B (read-only)
RELAY_FEDERATION_PEERS="wss://a.example|mirror:general,dev;wss://b.example|ingest:*"
# Simplest: fully mirror every channel with one peer
RELAY_FEDERATION_PEERS="wss://peer.example"
When federation is active the relay advertises a "federation" capability in
its NIP-11 document, so a Nosterm client knows the channel is distributed. Nothing changes
in how you chat — /join and send messages exactly as before.
Direct messages
Send an end-to-end encrypted private message with a single command. Nosterm uses NIP-17 gift-wrapped messages, so both the content and the metadata are protected.
/msg <npub-or-hex> hey, good to see you here
Look up who you're talking to with /whois <npub> — it shows the
profile and verifies their NIP-05 address (✓ / ✗) against its domain.
Themes & settings
Nosterm ships with twelve built-in terminal themes — Nord (the default), Gruvbox, Dracula, Tokyo Night, Solarized, Monokai and more — switchable live:
/theme list # show available themes
/theme set gruvbox-dark # apply a theme (persists across reloads)
/theme preview dracula # try one without committing
/theme reset # back to the default
Tune the terminal itself — font size, line height, motion, and sidebar visibility — with
/settings.
Command reference
Type /help in the app for the live list. The essentials:
Privacy & keys
Chat without surrendering your identity. Nosterm includes no third-party analytics and
loads no remote scripts. The only network connections it makes are to the relays you
choose and, for NIP-05 verification, read-only lookups to a domain's
/.well-known/nostr.json.
- Your private key is only used to sign; prefer an extension or remote signer so it never touches the page.
- Remembered keys are encrypted with your passphrase before being stored on the device.
- Cached messages live in your browser (IndexedDB) and are treated as untrusted on load.
Self-hosting
Nosterm is a static SPA and ships with an optional home relay, so you can run the whole stack yourself. There's nothing dynamic to host — the client is just static files — which keeps the deployment small and easy to reason about.
Recommended architecture
A three-part layout keeps concerns cleanly separated and works the same whether you're on a laptop, a VPS, or a Tor hidden service:
┌─────────────────────────────────────────┐
client ──▶ │ reverse proxy (Caddy) │
(TLS) │ ├─ / → static client (SPA) │
│ └─ /relay → home relay :3334 (ws) │
└─────────────────────────────────────────┘
- Reverse proxy (Caddy). A single public entry point. It serves the static client, terminates TLS (automatic HTTPS from a domain), and reverse-proxies WebSocket + NIP-11 traffic to the relay. It's also where you add security headers and, later, a Tor hidden service.
- Static client. The built SPA (plain HTML/JS/CSS). No server-side logic, no database — cache-friendly and trivially horizontally scalable.
-
Home relay (optional). A NIP-29 relay for your own managed rooms. Reached
by the client at
/relayso everything shares one origin — no CORS, no second hostname. Omit it entirely to point the client at public relays instead.
Putting the relay behind the same proxy under /relay means the browser only
ever opens connections to one origin, which keeps the Content-Security-Policy tight and
avoids mixed-content issues over HTTPS.
Running locally
For development or a quick look, run the client directly with Node. This gives you hot reload and doesn't require Docker.
Prerequisites
- Node.js 20+ and npm
The client (dev server)
npm install
npm run dev # http://localhost:5173 with hot reload
To build and preview the production bundle exactly as it will be served:
npm run build # outputs the static site to build/
npm run preview # serves build/ on http://localhost:4173
PUBLIC_DEFAULT_RELAYS (see .env.example). Point it at a public
relay, or run the home relay locally (next) and use ws://localhost:8080/relay.
The home relay (optional)
The relay lives in relay/ and can run on its own for local testing. See
relay/README.md for its configuration; the easiest path to a full local
stack is Docker Compose, below.
Running with Docker
The bundled docker-compose.yml builds and runs the whole stack — the client
(served by Caddy, which also reverse-proxies the relay) and the NIP-29 home relay — with
one command.
Prerequisites
- Docker and the Docker Compose plugin
Start the stack
docker compose up --build # add -d to run in the background
The client is then available at http://localhost:8080,
with the home relay reachable through the same origin at /relay.
Configuration
Set these as build args / environment variables (see .env.example):
openssl rand -hex 32 and set RELAY_SECRET_KEY — a fresh key each
boot corrupts signed rosters. Put a real domain in the Caddyfile for
automatic HTTPS.
Hosting on Tor for added privacy
Because Nosterm is static and speaks only to relays, it's a natural fit for Tor. Serving the client — and optionally your home relay — as an onion service means there's no public IP to correlate, no DNS to leak, and end-to-end encrypted transport for everyone who connects over Tor. It's a strong way to chat without surrendering your identity.
Architecture with Tor
Keep the same reverse-proxy layout and add a Tor daemon in front of it. Tor listens as a
hidden service and forwards to Caddy; Caddy still serves the client and proxies
/relay. Because both the client and relay sit behind one proxy on one origin,
a single .onion address covers the whole app.
Tor Browser ──▶ xxxx.onion (Tor hidden service)
│
▼
tor daemon ──▶ Caddy ──┬─ / static client
└─ /relay home relay (ws)
Add a Tor service to Docker Compose
Add a Tor container alongside the existing services. It shares the Compose network so it
can reach the nosterm proxy by name:
# docker-compose.tor.yml (merge with the base compose file)
services:
tor:
image: dperson/torproxy # or any maintained tor image
volumes:
- tor-data:/var/lib/tor # persists your .onion address + keys
- ./torrc:/etc/tor/torrc:ro
depends_on:
- nosterm
restart: unless-stopped
volumes:
tor-data:
A minimal torrc that exposes the proxy as a v3 onion service:
# torrc
HiddenServiceDir /var/lib/tor/nosterm
HiddenServiceVersion 3
# Forward onion port 80 to the Caddy container's port 80
HiddenServicePort 80 nosterm:80
docker compose -f docker-compose.yml -f docker-compose.tor.yml up --build -d
# Read your generated .onion address:
docker compose exec tor cat /var/lib/tor/nosterm/hostname
tor-data volume (specifically the
HiddenServiceDir). Those keys are your .onion address —
lose them and the address changes.
Notes for onion hosting
-
Skip public TLS. Tor already encrypts the transport end to end, so serve
plain HTTP inside the network (Caddy on
:80) and let Tor handle privacy. No certificate authority ever sees your service. -
Relative URLs matter. The client reaches its relay at
/relayon the same origin, so it works unchanged over.onion— no hardcoded public hostname to rewrite. -
Reachable from Tor Browser. Share the
.onionaddress; users open it in Tor Browser and connect with the same keys they'd use anywhere. -
Consider onion-only relays too. Pointing
PUBLIC_DEFAULT_RELAYSatws://onion relays keeps the entire message path inside Tor.