A hyper-private, E2EE messaging and voice/video system for a small, closed circle. The server is zero-knowledge (blind relay + temporary encrypted blobs only). No public signup; identities are generated once and distributed in person to minimize vulnerability (no keys or tokens over the network).
This repo has two parts:
relay-app/— Next.js web app that users run locally. They put their identity file in the app and point it at your server.relay-server/— Node.js API + Socket.io + background worker. You deploy this (e.g. on a VPS) and run the identity generator once to create the fixed set of accounts.
Running the web app locally is an intentional design choice so users can customize the UI and add features without touching the server. If you prefer to host the web app yourself, you can make a few changes and have users “log in” by drag-and-dropping their identity file in the browser. The identity file must always be processed in the browser (client-side) so the private key never leaves the user’s device; do not send or store the identity on the server.
- Fixed 20 accounts — One-time script generates 20 identity binaries; server is seeded with those key hashes. Only those keys can use the server.
- E2EE messaging — Text and files sent as uniform encrypted blobs (fixed size, chunked when too large): the server only sees opaque 64 KB blobs, so it cannot infer message length, file size, or content type. AES-GCM encryption, per-recipient keys. No local message storage.
- Voice/video — Room-based WebRTC (Socket.io signaling). Mute, deafen, camera, screen share.
- 72hr blob expiry with timestamp obfuscation — Blobs expire in a random window ~72 hours after the day they were sent; exact send time is not stored. The server cannot derive precise timestamps from expiry, and a worker deletes expired blobs.
- Identity — Binary file only. Out-of-band/in-person distribution is highly suggested for absolute security.
- Customizable app — UI and blob handling runs locally, so users can change the look and add their own features without touching the server or breaking compatibility.
These items come from an internal security review. They improve abuse resistance and metadata hardening; they do not change E2EE or decrypt blobs.
- Blob DELETE — Require proof the caller may delete (e.g. HMAC or signed token for recipient + blob id).
- Blob POST — Rate limit per recipient; consider proof-of-work or scoped tokens to reduce spam.
join_blob— Only emit refresh for clients that prove possession of the private key for thatkeyHash(e.g. challenge–response or short-lived token).- Call
join_room— Bind room ACL to server-verified allowed keys, or merge ACL when members join (avoid first-joiner-wins room squatting). - Signaling — Optionally encrypt signal payloads for the recipient’s key to reduce ICE/metadata exposure at the relay.
User-facing takeaway: Message content is intended to stay opaque to the relay. Metadata (who mails whom, when, call participation, IPs from ICE) is visible to the relay. Best mitigation: keep identity off the public web, use a relay you trust, verify contact keys out-of-band.
| Folder | Purpose |
|---|---|
relay-app/ |
Next.js app run locally by each user. Give users this folder + their identity file. Put their identity.bin in public/identity/ and set the server URL in env. |
relay-server/ |
Backend for the host machine only. Copy this folder to your cloud / VPS / server, configure env, run API + worker + DB. Identity generator usually runs once on an operator machine, then you deploy with allowed-keys-seed.json after seeding. |
- relay-app (users): Node.js v18+ (v20+ recommended), npm.
- relay-server (deployed): Node.js v18+, npm, PostgreSQL. For the identity generator: Node 19+ (Web Crypto API).
Done by whoever will operate the server. Generates the 20 identity files and the allowlist.
- Go into relay-server:
cd relay-server - Install deps and generate identities:
This creates:
npm i node scripts/generate-20-identities.mjs
identities/identity-01.bin…identity-20.bin(give one to each user)allowed-keys-seed.jsonin the repo root (used to seed the DB)
- Seed the database (after DB is set up and
DATABASE_URLis in.env):npx prisma db push # or migrate npm run seed - Give each user their package: send them the relay-app folder (same build for everyone) and their matching
identities/identity-NN.bin(ideally out-of-band/in-person). They rename/copy it toidentity.bininrelay-app/public/identity/identity.bin.
Voice/video uses WebRTC. The relay API only does signaling; media is peer-to-peer or relayed through TURN. For calls to work reliably, especially across different networks or strict NATs, TURN is effectively required. The app ships with public STUN only, which is enough for some same, LAN setups but not a substitute for TURN in the general case.
Today, Relay does not bundle a TURN server (nothing TURN-related runs inside the Node relay). I intend to bundle TURN with Relay in a future release so deployment is simpler. Until then, I recommend running coturn (free open source TURN) yourself, usually on the same VPS as relay-server.
Recommended setup (coturn) until bundled TURN ships:
- Install coturn on the host (e.g. Ubuntu:
apt install coturn). - Configure
/etc/turnserver.conf(paths may vary), for example:listening-port=3478- A static long-term credential user, e.g.
user=relay_turn:YOUR_STRONG_PASSWORD(or the equivalentlt-cred-mech+ user line your distro documents). - Set
min-port/max-portfor relayed media (coturn needs a UDP/TCP range for relay traffic).
- Firewall: allow 3478 (UDP and TCP as needed) and the configured relay port range on the same host’s public interface.
- relay-app: In each user’s
.env.local, set the three vars to match coturn (then restart dev or rebuild so Next picks them up):NEXT_PUBLIC_TURN_URL=turn:YOUR_SERVER_IP:3478NEXT_PUBLIC_TURN_USERNAME=relay_turnNEXT_PUBLIC_TURN_CREDENTIAL=YOUR_STRONG_PASSWORD
- Copy
.env.local.exampleto.env.localinrelay-app/. - Set:
NEXT_PUBLIC_API_URL— Your relay server URL (e.g.http://YOUR_SERVER_IP:4000orhttps://api.example.com).- TURN (required for reliable calls until Relay bundles TURN):
NEXT_PUBLIC_TURN_URL,NEXT_PUBLIC_TURN_USERNAME,NEXT_PUBLIC_TURN_CREDENTIAL— point these at your coturn instance (see §2).
- Copy
.env.exampleto.envinrelay-server/. - Set:
PORT— API port (default 4000).DATABASE_URL— PostgreSQL connection string.WEB_ORIGIN— Exact origin of the app (e.g.http://localhost:3000for local app, orhttps://relay.example.comif you ever host the app). Used for CORS.
On the machine that will host the relay server (cloud VM, VPS, e.g. a DigitalOcean droplet):
- Copy
relay-server/onto that machine (rsync, scp, git, etc.). - Install Node.js and PostgreSQL; create an empty database and a DB user.
- Configure env: copy
.env.example→.env, setDATABASE_URL,PORT(e.g. 4000), andWEB_ORIGIN(the origin users’ apps will use, e.g.http://localhost:3000). - Install deps and schema:
cd relay-server npm i npx prisma db push - Identity and seed: follow §1 Identity setup to generate the 20 identities; run
npm run seedso the DB has the allowed keys. - Start the relay:
npm run start # API + Socket.io npm run worker # deletes expired blobs (run in a second terminal or via PM2)
- Firewall: open the API port (e.g. 4000) and, if you use TURN, the TURN port(s).
- Users: point relay-app at
http://YOUR_HOST:PORTviaNEXT_PUBLIC_API_URLin their.env.local.
Each user receives relay-app + their identity binary from you. See relay-app/README.md: copy and edit .env.local.example → .env.local, put identity.bin in public/identity/, then:
npm i && npm run dev| Task | Where | Command / action |
|---|---|---|
| Generate 20 identities | relay-server | node scripts/generate-20-identities.mjs |
| Seed allowed keys | relay-server | npm run seed |
| Run API | relay-server | npm run start |
| Run expiry worker | relay-server | npm run worker |
| Run app locally | relay-app | Copy .env.local.example → .env.local, add identity.bin, then npm run dev |