talklan
v0.1.0
Published
Temporary local-network communication room. Start a server, scan the QR, talk. No cloud, no account, no internet required.
Readme
TalkLAN
Temporary local-network chat rooms. Start a server, scan the QR code with a phone, talk. No account, no app install, no cloud, no internet required.
Why
Sharing a quick conversation or a file with people on the same Wi-Fi should not require accounts, hosted services, or uploading data to someone else's server. TalkLAN runs one ephemeral room on your machine; everything lives in memory and disappears when the process exits.
Install
npm i -g talklan
# or run without installing:
npx talklanRequires Node.js >= 20.
Run
talklan # room on port 8787, expires in 30m, QR shown
talklan --room "Study group" --password s3cret --expires 1h
talklan -r "Team A" -r "Team B" # two rooms; --password applies to all of themThen join from a phone: scan the QR code printed in the terminal, or open
http://<lan-ip>:<port> in a mobile browser to reach the lobby. Pick a room,
pick a display name (and enter the password if the room has one) and chat.
Attach files with 📎, tap Reply
on any message to quote it, and use Invite in the header to show a QR + link
for the next person. The 🔔 button toggles message sounds. New rooms can also
be created from the lobby UI (optional password); up to 20 rooms per server.
Options
| Flag | Default | Description |
| ------------- | ------- | -------------------------------------------------- |
| --port | 8787 | Port to listen on. Default port busy? Next free port is tried automatically; an explicitly passed busy port fails with a clear error. |
| --room | host-based | Display name of a startup room; repeatable (-r A -r B starts one room per flag) |
| --password | none | Optional password applied to ALL startup rooms (never in URLs or logs) |
| --expires | 30m | Room lifetime: 10m, 30m, 1h, or 2h. The server shuts down automatically when time runs out. |
| --max-file-size | 50mb | Max upload size: bytes (1048576) or kb/mb/gb suffix (512kb, 50mb, 1gb) |
| --max-users | unlimited | Max participants (integer >= 1). Extra joins get a ROOM_FULL error and can retry later. |
| --history-file | none | Write history JSON ({exportedAt, rooms:[{id, room, startedAt, endedAt, messages}]}) to this path on shutdown |
| --no-qr | — | Suppress the terminal QR code |
| --no-browser| — | Do not open a browser on the host |
| --verbose | — | Verbose logging (still never logs secrets/content) |
| --help | — | Show help |
| --version | — | Show version |
Stop with Ctrl+C: the server stops accepting connections, notifies
clients, closes sockets, and deletes room state plus temp files.
(With --history-file, history JSON is written first.)
Replies
Send {type:"chat", content, replyTo:"<message-id>"} (or upload with
?senderId=…&roomId=…&replyTo=…) to quote another message. The server snapshots the
quote ({id, senderName, text} — chat text sliced to 140 chars, files as
File: <name>); client-supplied quote content is never trusted, and unknown
replyTo ids are silently ignored.
Lobby HTTP API
GET /api/rooms→{rooms: [{id, name, hasPassword, participantCount, messageCount}]}POST /api/rooms({name, password?}) →201 {id, name, hasPassword}; bad names →400 INVALID_NAME, more than 20 rooms →403 ROOM_LIMITGET /api/rooms/:id→{id, room, expiresAt, hasPassword, participantCount, maxUsers, messageCount}; unknown id →404 UNKNOWN_ROOMGET /api/qr[?room=<id>]→ SVG QR of the lobby (or one room's) join URL,image/svg+xml,no-store; unknown id →404 UNKNOWN_ROOM- WebSocket per room at
/ws/:id; join with{type:"join", name, password?}(no tokens in URLs anymore). Unknown room →{type:"error", code:"UNKNOWN_ROOM"}then close1008.
Security model
- LAN-only. No outbound requests, no telemetry, no analytics.
- Temporary by default. Room state, messages, uploads, and sessions live
in memory (
Map<string, Room/Client/Session>) and are gone on exit. - Central relay.
client → server → client(s)over WebSocket. No WebRTC peer-to-peer. - Server is authoritative. IDs, timestamps, and presence come from the
server; every WebSocket message is validated and malformed input gets an
errorreply, never a crash. Rate limits stop one device freezing the room. - Secrets stay secret. Passwords are never in URLs or logs. Tokens are
crypto.randomBytes-grade, expire with the room, and are never logged. - XSS-safe rendering. The web client uses
textContentonly, neverinnerHTML, for user content; usernames cannot contain HTML or spoofsystem. - Safe files. Uploads use generated internal IDs, sanitized names, size
limits, and streaming (no full-file buffering). Path traversal (
../, absolute paths, symlink escapes) is rejected. Temp files are deleted on room end/expiry.
Dev commands
npm install
npm run dev # local server (tsx)
npm run build # compile server (dist/) + web client (web/public/js/)
npm run test # unit + integration + security tests
npm run lint # typecheck both projects + forbid innerHTML
npm run typecheck # tsc --noEmit for server and web projects
npm run format # check formatting (node scripts/format.mjs --fix to fix)Architecture
phone browser ──┐
│ HTTP (static UI + /api/*) / WebSocket (chat relay)
laptop browser ─┼──▶ talklan server (Node.js, in-memory room) ──▶ LAN IP + QR
phone browser ──┘
▲ │
└──── file download ── upload ── chat/presence/typing ──┘src/cli/— args, terminal UIsrc/server/— HTTP server, WebSocket relay, rooms, clients, routersrc/network/— LAN interface/IP detection, QRsrc/messages/— protocol types, validation, handlerssrc/files/— upload/download relay, filename securitysrc/auth/— room token, sessionssrc/config/— defaultsweb/— staticindex.htmlplus TypeScript client (app.ts,websocket.ts,state.ts,api.ts), compiled withtsconfig.web.json(no bundler, no npm dependencies for the web client)tests/unit|integration|security— unit, integration, and security tests (XSS, traversal, malformed WS, oversized message/upload, bad token, flood, malicious filename)scripts/— lint/format helpers
The WebSocket protocol is versionable. Client → server: join, chat,
typing, ping. Server → client: joined, chat, file, presence,
typing, system, error, shutdown, pong.
No-cloud statement
TalkLAN works fully offline on a local network. It does not use Firebase, Supabase, hosted databases, external auth, or any third-party service, and it sends no data anywhere except between devices on your LAN.
License
MIT — see LICENSE.
