Docs Launch client

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.

In one line: channels without servers, identity without passwords.

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

  1. Open the client and pick a way to sign in (or explore with a temporary identity).
  2. You're connected to a default relay automatically. Type /help to see every command.
  3. Join a channel: /join #general.
  4. Say hello — just type and press Enter. Text without a leading / is a message to the active room.
  5. Set a display name others will see: /nick yourname.
Tip: press 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)

⚠ Advanced: pasting an 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 mirror peer is bidirectional — chat posted on either relay is forwarded to the other. An ingest peer 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↔A loops 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.

Trust model: federate only with relays you trust to authorize their own posters — a federated channel behaves as open across every participating relay. See Self-hosting to run your own federated relay.

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:

/help [command]List commands, or usage for one
/connect <relay-url>Connect to a relay
/relays [discover]List relays; discover your NIP-65 list
/join <#room>Join a NIP-29 room
/part [room]Leave a room (defaults to active)
/roomsList joined rooms
/switch <room>Change the active room
/me <action>Send an action message
/msg <npub> <message>Send an encrypted DM (NIP-17)
/nick <name>Set your public display name
/whoList recently-active users in the room
/whois <npub>Show a profile; verify NIP-05
/invite <npub>Admin: add a user to the room
/kick <npub>Admin: remove a user from the room
/mute · /unmute <npub>Hide/unhide a user (local only)
/theme [list|set|preview|reset]View or change the color theme
/settings [key value]View/change font, motion, sidebar
/statusShow identity, relays, rooms, theme
/clearClear the terminal
/login [method]Sign in (bare opens the sign-in screen)
/logoutLog out and detach the signer

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 /relay so 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
Relays: by default the dev client connects to whatever you set in 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):

PUBLIC_DEFAULT_RELAYSRelays the client connects to on start
PUBLIC_DEFAULT_THEMEDefault color theme (e.g. nord)
RELAY_SECRET_KEYStable relay identity — required in production
RELAY_NAMEHuman-readable relay name
RELAY_RETENTION_DAYSPrune chat messages older than N days (0 = keep)
RELAY_FEDERATION_PEERSPeer relays to federate chat with (empty = off)
⚠ Production: generate a stable relay key with 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
Tip: back up the 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 /relay on the same origin, so it works unchanged over .onion — no hardcoded public hostname to rewrite.
  • Reachable from Tor Browser. Share the .onion address; users open it in Tor Browser and connect with the same keys they'd use anywhere.
  • Consider onion-only relays too. Pointing PUBLIC_DEFAULT_RELAYS at ws:// onion relays keeps the entire message path inside Tor.