7.3 KiB
Nosterm
A terminal-style Nostr chat client.
The Nosterm web client: a terminal-style Nostr chat SPA built with SvelteKit and served as static files by Caddy. Nostr is a client-side protocol, so there is no application backend here — the browser talks directly to relays over WebSocket. Run a relay from the companion nosterm-relay repository if you want your own home relay.
This is the public, contribution-focused repository for the Nosterm client.
Open a pull request against main — see CONTRIBUTING.md. Each
merge to main builds the container image and publishes it to ECR Public for
anonymous pulls (see Deploy).
Mission
Nosterm aims to be the premier, full-featured Nostr Relay Chat (NRC) client — the terminal-native way to talk over Nostr. We believe private communication is a right, not a feature, so Nosterm is built to advocate for and promote anonymity, end-to-end encryption, and censorship resistance by default rather than as opt-in extras.
Three principles guide every decision:
- Small footprint. A static SPA with no application backend, served from a single lightweight container. It should stay auditable, fast to load, and cheap to self-host — no build-time secrets, no telemetry, no lock-in.
- Easy to deploy, anywhere. One prebuilt image, configured at runtime, that drops cleanly onto different overlay network protocols (Tor, I2P, Yggdrasil, Nym, and WireGuard-style meshes) so operators can run it wherever they trust the transport.
- Privacy as the default. Anonymity, encryption, and anti-censorship are the baseline behavior. The client should make the private path the easy path.
Roadmap
The roadmap turns the mission into concrete direction. Items are aspirational and unordered — contributions that move any of them forward are welcome.
- Full NRC feature set — channels, threads, DMs, presence, and moderation tools that make Nosterm a complete relay-chat experience, not just a demo.
- Overlay-network first — first-class, documented deployment recipes for Tor hidden services, I2P, and other overlay transports, keeping the same single runtime-configured image.
- Encryption everywhere — modern encrypted DMs and private groups (e.g. NIP-17 / gift-wrapped events), with sensible key handling and no plaintext fallbacks that surprise the user.
- Anonymity by design — minimize metadata leakage, support ephemeral and disposable identities, and avoid any client behavior that fingerprints or phones home.
- Censorship resistance — graceful multi-relay failover, easy relay discovery and rotation, and resilience when individual relays are blocked or disappear.
- Mature the Nostermd companion relay — grow the nostermd-relay home relay into a first-class NIP-29 (relay-based groups) implementation, deepening its federation and group-chat features so a self-hosted relay is a complete NRC backend for the client.
- Stay small — hold the line on bundle size, dependency count, and container footprint as features grow.
Layout
nosterm-client/
src/ # SvelteKit source (components, stores, nostr, commands…)
static/ # static assets
Dockerfile # build the SPA → serve with Caddy
Caddyfile # static file server + security headers + optional /relay proxy
docker-compose.yml # one-command deploy on port 8080
.env.example # copy to .env and configure
package.json # dev/build/test scripts
Develop
npm install
npm run dev # vite dev server
npm run check # svelte-check (type-check)
npm run lint # prettier --check + eslint
npm test # vitest
npm run test:e2e # playwright
npm run build # static build → ./build
Deploy
cp .env.example .env # then set NOSTERM_DEFAULT_RELAYS at minimum
docker compose up -d --build
The client is then served at http://localhost:8080.
To run the prebuilt public image with your own relays — no rebuild, no AWS account (anonymous pull from ECR Public):
docker run -p 8080:80 \
-e NOSTERM_DEFAULT_RELAYS=wss://relay.example.com \
public.ecr.aws/k3k1z1x5/nosterm:latest
NOSTERM_DEFAULT_RELAYS accepts a comma-separated list. On startup the
container's entrypoint writes it to /config.json, which the SPA fetches on
load — so one shared image serves every deployment with different relays.
Configuration
Relays are configured at runtime (NOSTERM_DEFAULT_RELAYS), so a single
prebuilt image works for any deployment. The remaining PUBLIC_* tunables are
build-time args baked into the browser bundle (client-visible, never
secrets) — change one → rebuild the image.
Runtime (set on the running container, no rebuild):
| Variable | Default | Purpose |
|---|---|---|
NOSTERM_DEFAULT_RELAYS |
empty | Comma-separated relay WebSocket URLs to preload |
RELAY_UPSTREAM |
relay:3334 |
Upstream for Caddy's optional /relay reverse-proxy (below) |
Build-time (baked into the bundle; rebuild to change):
| Variable | Default | Purpose |
|---|---|---|
PUBLIC_DEFAULT_RELAYS |
empty | Fallback relays when no runtime config is set |
PUBLIC_DEFAULT_THEME |
nord |
Theme applied before the user picks one |
PUBLIC_DEFAULT_CHANNEL |
nosterm |
Channel auto-joined on startup (blank = disabled) |
PUBLIC_MAX_TERMINAL_ENTRIES |
1000 |
Terminal entries kept in memory / rendered |
PUBLIC_MAX_MESSAGE_LENGTH |
2000 |
Max outbound message length (characters) |
PUBLIC_MAX_CACHED_MESSAGES_PER_ROOM |
500 |
Messages retained per room in IndexedDB |
Talking to a relay
The client connects directly to whatever relays are configured in
NOSTERM_DEFAULT_RELAYS (and any the user adds at runtime). Two common setups:
- External relay — set
NOSTERM_DEFAULT_RELAYS=wss://relay.example.com. You can delete thehandle /relay*block from theCaddyfile. - Same-origin home relay — run
nosterm-relay, set
NOSTERM_DEFAULT_RELAYS=ws://localhost:8080/relay(orwss://…behind TLS), and setRELAY_UPSTREAMto the relay's address so Caddy proxies/relayto it. Put both services on a shared Docker network for name resolution.
Production TLS
For a real domain, edit the Caddyfile: replace :80 with your hostname (e.g.
nosterm.example.com { … }) and Caddy will obtain and renew a certificate
automatically. Expose ports 80 and 443 in docker-compose.yml.