Auto-join #nosterm on startup; add mission/roadmap; chat-window banner #1

Merged
rgoodall merged 1 commits from feat/auto-join-and-readme into main 2026-07-25 12:25:36 +00:00
8 changed files with 161 additions and 33 deletions
Showing only changes of commit dd388061d1 - Show all commits
+4
View File
@@ -26,6 +26,10 @@ PUBLIC_DEFAULT_RELAYS=
# Default UI theme id. # Default UI theme id.
PUBLIC_DEFAULT_THEME=nord PUBLIC_DEFAULT_THEME=nord
# Channel auto-joined on startup, on the first default relay (bare name, no '#').
# Leave blank to disable auto-join.
PUBLIC_DEFAULT_CHANNEL=nosterm
# Max lines kept in the terminal scrollback. # Max lines kept in the terminal scrollback.
PUBLIC_MAX_TERMINAL_ENTRIES=1000 PUBLIC_MAX_TERMINAL_ENTRIES=1000
+3
View File
@@ -15,3 +15,6 @@ playwright-report/
!.env.example !.env.example
.DS_Store .DS_Store
# local AI assistant instructions
CLAUDE.local.md
+2
View File
@@ -29,11 +29,13 @@ RUN npm ci
COPY . ./ COPY . ./
ARG PUBLIC_DEFAULT_RELAYS="" ARG PUBLIC_DEFAULT_RELAYS=""
ARG PUBLIC_DEFAULT_THEME="nord" ARG PUBLIC_DEFAULT_THEME="nord"
ARG PUBLIC_DEFAULT_CHANNEL="nosterm"
ARG PUBLIC_MAX_TERMINAL_ENTRIES="1000" ARG PUBLIC_MAX_TERMINAL_ENTRIES="1000"
ARG PUBLIC_MAX_MESSAGE_LENGTH="2000" ARG PUBLIC_MAX_MESSAGE_LENGTH="2000"
ARG PUBLIC_MAX_CACHED_MESSAGES_PER_ROOM="500" ARG PUBLIC_MAX_CACHED_MESSAGES_PER_ROOM="500"
ENV PUBLIC_DEFAULT_RELAYS=$PUBLIC_DEFAULT_RELAYS \ ENV PUBLIC_DEFAULT_RELAYS=$PUBLIC_DEFAULT_RELAYS \
PUBLIC_DEFAULT_THEME=$PUBLIC_DEFAULT_THEME \ PUBLIC_DEFAULT_THEME=$PUBLIC_DEFAULT_THEME \
PUBLIC_DEFAULT_CHANNEL=$PUBLIC_DEFAULT_CHANNEL \
PUBLIC_MAX_TERMINAL_ENTRIES=$PUBLIC_MAX_TERMINAL_ENTRIES \ PUBLIC_MAX_TERMINAL_ENTRIES=$PUBLIC_MAX_TERMINAL_ENTRIES \
PUBLIC_MAX_MESSAGE_LENGTH=$PUBLIC_MAX_MESSAGE_LENGTH \ PUBLIC_MAX_MESSAGE_LENGTH=$PUBLIC_MAX_MESSAGE_LENGTH \
PUBLIC_MAX_CACHED_MESSAGES_PER_ROOM=$PUBLIC_MAX_CACHED_MESSAGES_PER_ROOM PUBLIC_MAX_CACHED_MESSAGES_PER_ROOM=$PUBLIC_MAX_CACHED_MESSAGES_PER_ROOM
+49 -1
View File
@@ -19,6 +19,53 @@ Open a pull request against `main` — see [CONTRIBUTING.md](CONTRIBUTING.md). E
merge to `main` builds the container image and publishes it to ECR Public for merge to `main` builds the container image and publishes it to ECR Public for
anonymous pulls (see [Deploy](#deploy)). anonymous pulls (see [Deploy](#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](https://git.nerdworks.io/nerdworks/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 ## Layout
``` ```
@@ -83,9 +130,10 @@ secrets) — change one → rebuild the image.
**Build-time** (baked into the bundle; rebuild to change): **Build-time** (baked into the bundle; rebuild to change):
| Variable | Default | Purpose | | Variable | Default | Purpose |
| ------------------------------------- | ------- | --------------------------------------------- | | ------------------------------------- | --------- | ------------------------------------------------- |
| `PUBLIC_DEFAULT_RELAYS` | _empty_ | Fallback relays when no runtime config is set | | `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_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_TERMINAL_ENTRIES` | `1000` | Terminal entries kept in memory / rendered |
| `PUBLIC_MAX_MESSAGE_LENGTH` | `2000` | Max outbound message length (characters) | | `PUBLIC_MAX_MESSAGE_LENGTH` | `2000` | Max outbound message length (characters) |
| `PUBLIC_MAX_CACHED_MESSAGES_PER_ROOM` | `500` | Messages retained per room in IndexedDB | | `PUBLIC_MAX_CACHED_MESSAGES_PER_ROOM` | `500` | Messages retained per room in IndexedDB |
+77 -25
View File
@@ -1,8 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="820" height="240" viewBox="0 0 820 240" role="img" aria-label="Nosterm — a terminal-style Nostr chat application"> <svg xmlns="http://www.w3.org/2000/svg" width="820" height="320" viewBox="0 0 820 320" role="img" aria-label="Nosterm — a terminal-style Nostr chat window with user and relay lists">
<title>Nosterm — a terminal-style Nostr chat application</title> <title>Nosterm — a terminal-style Nostr chat window with user and relay lists</title>
<!-- Nord palette: bg #2e3440, surface #3b4252, frame #434c5e, <!-- Nord palette: bg #2e3440, surface #3b4252, frame #434c5e, muted #4c566a,
cyan #88c0d0, snow #eceff4, comment #616e88, green #a3be8c --> comment #616e88, snow #d8dee9/#eceff4, frost #81a1c1/#88c0d0,
aurora red #bf616a, yellow #ebcb8b, green #a3be8c, purple #b48ead -->
<defs> <defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#3b4252" /> <stop offset="0" stop-color="#3b4252" />
@@ -11,34 +12,85 @@
</defs> </defs>
<!-- outer canvas --> <!-- outer canvas -->
<rect width="820" height="240" rx="14" fill="#2e3440" /> <rect width="820" height="320" rx="14" fill="#2e3440" />
<!-- terminal window --> <!-- chat window -->
<rect x="40" y="34" width="740" height="172" rx="10" fill="url(#bg)" stroke="#434c5e" stroke-width="1.5" /> <rect x="30" y="26" width="760" height="268" rx="10" fill="url(#bg)" stroke="#434c5e" stroke-width="1.5" />
<!-- title bar --> <!-- title bar -->
<rect x="40" y="34" width="740" height="34" rx="10" fill="#434c5e" /> <rect x="30" y="26" width="760" height="34" rx="10" fill="#434c5e" />
<rect x="40" y="58" width="740" height="10" fill="#434c5e" /> <rect x="30" y="50" width="760" height="10" fill="#434c5e" />
<circle cx="64" cy="51" r="5.5" fill="#bf616a" /> <circle cx="54" cy="43" r="5.5" fill="#bf616a" />
<circle cx="84" cy="51" r="5.5" fill="#ebcb8b" /> <circle cx="74" cy="43" r="5.5" fill="#ebcb8b" />
<circle cx="104" cy="51" r="5.5" fill="#a3be8c" /> <circle cx="94" cy="43" r="5.5" fill="#a3be8c" />
<text x="410" y="55" text-anchor="middle" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13" fill="#616e88">nosterm — /home/you</text> <text x="410" y="47" text-anchor="middle" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13" fill="#616e88">nosterm — #general</text>
<!-- sidebar divider -->
<line x1="598" y1="60" x2="598" y2="294" stroke="#434c5e" stroke-width="1.5" />
<!-- terminal body -->
<g font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"> <g font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">
<!-- wordmark: prompt + name --> <!-- ===== chat transcript ===== -->
<text x="70" y="126" font-size="46" font-weight="700" fill="#88c0d0">&gt;_</text> <g font-size="13">
<text x="152" y="126" font-size="46" font-weight="700" fill="#eceff4">nosterm</text> <!-- msg 1 -->
<text x="50" y="92" fill="#616e88">09:42</text>
<text x="104" y="92" fill="#88c0d0">@satoshi</text>
<text x="190" y="92" fill="#d8dee9">gm — anyone on a home relay?</text>
<!-- msg 2 -->
<text x="50" y="120" fill="#616e88">09:43</text>
<text x="104" y="120" fill="#a3be8c">@you</text>
<text x="190" y="120" fill="#d8dee9">yeah: ws://localhost/relay</text>
<!-- msg 3 -->
<text x="50" y="148" fill="#616e88">09:44</text>
<text x="104" y="148" fill="#ebcb8b">@nostrich</text>
<text x="190" y="148" fill="#d8dee9">clean. nostr, in a terminal.</text>
<!-- msg 4 -->
<text x="50" y="176" fill="#616e88">09:45</text>
<text x="104" y="176" fill="#b48ead">@hodl</text>
<text x="190" y="176" fill="#d8dee9">/theme nord</text>
<text x="286" y="176" fill="#a3be8c"></text>
<!-- msg 5 -->
<text x="50" y="204" fill="#616e88">09:46</text>
<text x="104" y="204" fill="#88c0d0">@satoshi</text>
<text x="190" y="204" fill="#d8dee9">🔥 no backend, just relays</text>
<!-- msg 6 -->
<text x="50" y="232" fill="#616e88">09:47</text>
<text x="104" y="232" fill="#a3be8c">@you</text>
<text x="190" y="232" fill="#d8dee9">signed &amp; broadcast. gg</text>
</g>
<!-- tagline as a shell comment --> <!-- input line -->
<text x="72" y="162" font-size="16" fill="#616e88"># a terminal-style Nostr chat client + home relay</text> <line x1="46" y1="256" x2="566" y2="256" stroke="#434c5e" stroke-width="1" />
<text x="50" y="282" font-size="15" fill="#88c0d0">&gt;</text>
<!-- a sample command line --> <text x="68" y="282" font-size="15" fill="#eceff4">/msg #general </text>
<text x="72" y="190" font-size="16" fill="#a3be8c">$</text> <rect x="200" y="270" width="8" height="16" fill="#88c0d0">
<text x="92" y="190" font-size="16" fill="#d8dee9">/join</text>
<text x="150" y="190" font-size="16" fill="#88c0d0">#general</text>
<rect x="238" y="177" width="9" height="17" fill="#88c0d0">
<animate attributeName="opacity" values="1;1;0;0" dur="1.1s" repeatCount="indefinite" /> <animate attributeName="opacity" values="1;1;0;0" dur="1.1s" repeatCount="indefinite" />
</rect> </rect>
<!-- ===== sidebar: users ===== -->
<text x="618" y="88" font-size="11" letter-spacing="1.5" fill="#616e88">USERS · 4</text>
<g font-size="13">
<circle cx="624" cy="110" r="4" fill="#a3be8c" />
<text x="640" y="114" fill="#d8dee9">satoshi</text>
<circle cx="624" cy="134" r="4" fill="#a3be8c" />
<text x="640" y="138" fill="#d8dee9">nostrich</text>
<circle cx="624" cy="158" r="4" fill="#88c0d0" />
<text x="640" y="162" fill="#88c0d0">you</text>
<circle cx="624" cy="182" r="4" fill="#4c566a" />
<text x="640" y="186" fill="#616e88">hodl</text>
</g>
<!-- sidebar divider between sections -->
<line x1="614" y1="204" x2="774" y2="204" stroke="#434c5e" stroke-width="1" />
<!-- ===== sidebar: relays ===== -->
<text x="618" y="228" font-size="11" letter-spacing="1.5" fill="#616e88">RELAYS · 3</text>
<g font-size="12">
<circle cx="624" cy="250" r="4" fill="#a3be8c" />
<text x="640" y="254" fill="#d8dee9">relay.damus.io</text>
<circle cx="624" cy="272" r="4" fill="#a3be8c" />
<text x="640" y="276" fill="#d8dee9">nos.lol</text>
<circle cx="624" cy="290" r="4" fill="#bf616a" />
<text x="640" y="294" fill="#616e88">relay.snort.social</text>
</g>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

+1
View File
@@ -16,6 +16,7 @@ services:
# Non-relay tunables are still baked in at build time. Relays are set at # Non-relay tunables are still baked in at build time. Relays are set at
# runtime via NOSTERM_DEFAULT_RELAYS below (no rebuild to change them). # runtime via NOSTERM_DEFAULT_RELAYS below (no rebuild to change them).
PUBLIC_DEFAULT_THEME: ${PUBLIC_DEFAULT_THEME:-nord} PUBLIC_DEFAULT_THEME: ${PUBLIC_DEFAULT_THEME:-nord}
PUBLIC_DEFAULT_CHANNEL: ${PUBLIC_DEFAULT_CHANNEL:-nosterm}
PUBLIC_MAX_TERMINAL_ENTRIES: ${PUBLIC_MAX_TERMINAL_ENTRIES:-1000} PUBLIC_MAX_TERMINAL_ENTRIES: ${PUBLIC_MAX_TERMINAL_ENTRIES:-1000}
PUBLIC_MAX_MESSAGE_LENGTH: ${PUBLIC_MAX_MESSAGE_LENGTH:-2000} PUBLIC_MAX_MESSAGE_LENGTH: ${PUBLIC_MAX_MESSAGE_LENGTH:-2000}
PUBLIC_MAX_CACHED_MESSAGES_PER_ROOM: ${PUBLIC_MAX_CACHED_MESSAGES_PER_ROOM:-500} PUBLIC_MAX_CACHED_MESSAGES_PER_ROOM: ${PUBLIC_MAX_CACHED_MESSAGES_PER_ROOM:-500}
+8
View File
@@ -20,6 +20,14 @@ export const DEFAULT_RELAYS: string[] = (env.PUBLIC_DEFAULT_RELAYS ?? '')
export const DEFAULT_THEME_ID: string = (env.PUBLIC_DEFAULT_THEME ?? 'nord').trim() || 'nord'; export const DEFAULT_THEME_ID: string = (env.PUBLIC_DEFAULT_THEME ?? 'nord').trim() || 'nord';
/**
* channel auto-joined on startup, on the first default relay. bare group name
* (no leading '#'). set to empty to disable auto-join.
*/
export const DEFAULT_CHANNEL: string = (env.PUBLIC_DEFAULT_CHANNEL ?? 'nosterm')
.trim()
.replace(/^#/, '');
export const MAX_TERMINAL_ENTRIES: number = parseIntOr(env.PUBLIC_MAX_TERMINAL_ENTRIES, 1000); export const MAX_TERMINAL_ENTRIES: number = parseIntOr(env.PUBLIC_MAX_TERMINAL_ENTRIES, 1000);
export const MAX_MESSAGE_LENGTH: number = parseIntOr(env.PUBLIC_MAX_MESSAGE_LENGTH, 2000); export const MAX_MESSAGE_LENGTH: number = parseIntOr(env.PUBLIC_MAX_MESSAGE_LENGTH, 2000);
+11 -1
View File
@@ -14,6 +14,8 @@
import { uiStore } from '$lib/stores/ui-store'; import { uiStore } from '$lib/stores/ui-store';
import { pluginHost } from '$lib/plugins/plugin-host'; import { pluginHost } from '$lib/plugins/plugin-host';
import { loadDefaultRelays } from '$lib/runtime-config'; import { loadDefaultRelays } from '$lib/runtime-config';
import { DEFAULT_CHANNEL } from '$lib/config';
import { roomService } from '$lib/nostr/room-service';
import { BRAND } from '$lib/brand'; import { BRAND } from '$lib/brand';
const { dispatcher, registry } = createAppDispatcher(); const { dispatcher, registry } = createAppDispatcher();
@@ -35,11 +37,19 @@
// relays come from runtime /config.json (per-deployment, no rebuild) // relays come from runtime /config.json (per-deployment, no rebuild)
// and fall back to the build-time defaults. // and fall back to the build-time defaults.
const relays = await loadDefaultRelays(); const relays = await loadDefaultRelays();
if (relays.length > 0) { const [firstRelay] = relays;
if (firstRelay) {
terminalStore.system(`Connecting to ${relays.length} relay(s)…`, 'system'); terminalStore.system(`Connecting to ${relays.length} relay(s)…`, 'system');
for (const url of relays) { for (const url of relays) {
void relayService.connect(url); void relayService.connect(url);
} }
// auto-join the default channel on the first relay so the app opens on a
// room instead of an empty terminal. join() handles waiting for the relay
// to connect; a NIP-29 join request needs no login (reading is open).
if (DEFAULT_CHANNEL) {
void roomService.join({ relayUrl: firstRelay, groupId: DEFAULT_CHANNEL });
}
} else { } else {
terminalStore.system('No default relays configured. Use /connect <relay-url>.', 'system'); terminalStore.system('No default relays configured. Use /connect <relay-url>.', 'system');
} }