@biocrypt/private-chat
v1.0.9
Published
Self-hostable, zero-dep private DNA-chat: ships a WebSocket relay + minimal browser UI. Everything (relay URL, room id, host public key) rides inside a single base64url link fragment. The server never sees plaintext.
Downloads
2,136
Maintainers
Readme
@biocrypt/private-chat
npmjs.com: The rendered README appears on the package’s latest tab. If you open an older version from the “Versions” list, the site often shows “no README” even though the tarball contains
README.md—use latest or read the same file on GitHub.
Self-hostable WebSocket relay + browser UI for small private chats. One npm package serves static files and the relay; invite links carry room id and host public key in the URL fragment (never sent to the server).
- Pure ESM, no runtime npm dependencies in the Node relay/server.
- RSA-DNA end-to-end encryption; the relay only sees ciphertext.
- 1:1 messaging: each recipient gets their own encrypted copy of a message.
- Link creator approves joiners; joiners use an ephemeral keypair per tab (nothing written to disk).
- No accounts, no message history on the server.
Hosted demo: private.biocrypt.net
Run it
npx @biocrypt/private-chatDefaults: port 8787, host 0.0.0.0 (browser URL shown as http://localhost:8787). Then open the URL in a browser.
npx @biocrypt/private-chat --port 9000
npx @biocrypt/private-chat --host 127.0.0.1 --port 8787
PORT=9000 HOST=127.0.0.1 npx @biocrypt/private-chatServe a different UI build (must include index.html and assets):
npx @biocrypt/private-chat --static ./path/to/static
# or STATIC_DIR=./path/to/staticnpx @biocrypt/private-chat --helpDeploy tip: GET /healthz returns JSON { ok, clients } for probes.
Using the app (what the UI actually does)
- Create link — Accept the short notice, then create a room. You get an invite URL and can Share / Copy / Continue (enter as link creator).
- Link creator — Approve or decline join requests in Requests. Declining sends a deny to that joiner so they stop waiting.
- Join — Open the invite link, enter a name, Enter. If the creator is not in the room yet, join requests retry about every 5 seconds until admitted or denied.
- Chat — Pick people in People, type in the bar, send. Messages are encrypted to each selected peer.
Host session: The link creator’s keys stay in memory for as long as this page stays loaded (same tab, no timeout for idleness or background tabs). A copy of the host private key is also written to sessionStorage so the same load can restore you as host after in-app hash changes. Reloading, closing the tab, or navigating away runs a normal page unload: storage is cleared and keys are gone — there is no “paste your private key” screen. After that, the invite link still works for new joiners, but this browser session is no longer the link creator until someone runs Create link again.
Invite link format
https://<page>/#c=<base64url(JSON)>| Field | Meaning |
| --- | --- |
| v | Bundle version (1) |
| r | Room id (32 hex chars, 16 bytes) |
| h | Link creator’s public key (DNA, ACGT…) |
| w | (optional) WebSocket URL; if omitted, the page uses same origin (ws: / wss: from the page URL) |
The fragment is not sent on HTTP requests, so ordinary access logs do not contain room or host key.
Library API
import { createServer, createRelay, buildLink, parseLink } from "@biocrypt/private-chat";
// HTTP static + WebSocket relay (same as the CLI)
const { server, relay, staticDir } = createServer();
server.listen(8787);
// Only the relay on your own `http.Server`
import http from "node:http";
const app = http.createServer((req, res) => res.end("ok"));
const relay = createRelay();
app.on("upgrade", (req, sock) => relay.onUpgrade(req, sock));
app.listen(3000);
// Build / parse links (Node or compatible bundler)
const link = buildLink({
baseUrl: "https://example.com/app/",
room: "…", // hex room id
hostPubDna: "ACGT…",
// wsUrl: "wss://example.com/", // optional
});
const { v, room, hostPubDna, wsUrl } = parseLink(link);Wire protocol (gemix-private)
Frames are normal relay msg envelopes with meta.proto === "gemix-private", meta.r = room, meta.toPubHash = SHA-256 of recipient public DNA, meta.k = kind. Ciphertext payload is JSON.
| k | Payload (decrypted) | Direction |
| --- | --- | --- |
| join | { codename, sessionPubDna, ts } | joiner → creator |
| admit | { room, hostPubDna, codename, admittedAt, roster } | creator → joiner |
| deny | { at, sessionPubDna } | creator → joiner |
| peer | { pubDna, codename, isHost } | creator → members (roster update) |
| chat | { text, ts } | peer → peer |
| bye | { reason? } | (reserved) |
Intentionally out of scope
- No server-side chat history or sync beyond live relaying.
- No built-in group broadcast (you select recipients per message).
- No typing/presence in the stock UI.
License
MIT
