Open for work
code/ nulled-room

End-to-end encrypted ephemeral chat running on a VPS with WebSockets under PM2.

MIT 4 files 46.4 KB xml 2 downloads Updated 15 hr ago
nulled-room
▢index.html 37.2 KB 16 hr ago
▢nulledroom.nginx.conf 1 KB 16 hr ago
▢package.json 288 B 16 hr ago
▢server.js 7.9 KB 16 hr ago
README

NulledRoom

E2EE ephemeral chat. No logs. No history. No trace.

A self-hosted, end-to-end encrypted chat room with a terminal aesthetic. Messages are encrypted in the browser before they hit the wire. The server never sees plaintext. No accounts, no logs, no message history — when the room is gone, it's gone.


What it is

NulledRoom is a WebSocket-based chat app where:

  • All messages are encrypted client-side with AES-GCM-256 before transmission
  • The room ID is the key — only people who know the room ID can derive the encryption key
  • Nothing is persisted — all state is in-memory; when the server restarts or a room empties, everything disappears
  • No accounts — users get random hacker handles (null_socket_42, ghost_daemon_17, etc.)
  • Images are supported — compressed client-side, encrypted, sent as encrypted blobs
  • The URL never changes — room ID is never in the URL, so browser history reveals nothing


How E2EE works

The room ID (e.g. XK7P2Q) is used as the key material. When a user creates or joins a room, the browser runs:

PBKDF2(roomId, salt="nulledroom-salt-v1", iterations=100000, hash=SHA-256) → AES-GCM-256 key


All messages are encrypted with a random 12-byte IV before sending, and the server only ever sees an opaque base64 blob. Decryption happens client-side on receipt. If someone intercepts the WebSocket traffic, they see nothing useful without the room ID.


Tech stack

Layer | What

Backend | Node.js + ws (WebSocket library)
Frontend | Vanilla JS + Web Crypto API, single HTML file
Proxy | Nginx (SSL termination + WebSocket forwarding)
Process manager | PM2
Fonts | VT323 + Share Tech Mono (Google Fonts)
Hosting | Hetzner CPX11 VPS (Ubuntu 24.04)
Domain/SSL | Porkbun + Let's Encrypt / Certbot

Features

  • Create room with configurable options:
  • Inactivity timeout: 5 min to 24 hours (idle users get kicked)
  • Nuke on host leave: if enabled, all users are disconnected when the creator leaves
  • Join room with a 6-character alphanumeric code
  • Room ID banner shown large and in-your-face after creation (easy to read on mobile)
  • Image attachments: client-side canvas compression (max 20MB input, targets ~1.4MB after compression), E2EE before sending, click to expand in lightbox
  • Silent guest reconnect: if a guest's connection drops (e.g. phone backgrounds the browser), they silently rejoin up to 8 times without spamming "[name] disconnected" to the room
  • Nuke button for room creator — instantly destroys the room and boots everyone with a "NUKED" overlay
  • Mobile-responsive terminal/CRT aesthetic (VT323 + Share Tech Mono, green-on-black, scanline overlay)
  • Online users panel (hidden on small screens)

File structure

nulledroom/
├── server.js # Node.js WebSocket server
├── package.json # Single dependency: ws
├── index.html # Entire frontend (one file)
└── nulledroom.nginx.conf # Nginx config (for reference)


On the VPS:

/var/www/nulledroom/index.html # Frontend served by Nginx
/opt/nulledroom/server.js # Backend (wherever you put it)
/etc/nginx/sites-available/nulledroom # Nginx config

Prerequisites

  • A VPS running Ubuntu 22.04 or 24.04 (Hetzner CPX11 works fine)
  • A domain pointing to your VPS IP (A record)
  • Node.js 18+ installed
  • PM2 installed globally
  • Nginx installed
  • Certbot installed

Installation / Deploy from scratch


1. Server setup

bash
# Update system
sudo apt update && sudo apt upgrade -y
# Install Node.js (v20 LTS)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
# Install PM2
sudo npm install -g pm2
# Install Nginx
sudo apt install -y nginx
# Install Certbot
sudo apt install -y certbot python3-certbot-nginx

2. Deploy backend

bash
# Create app directory
mkdir -p /opt/nulledroom
cd /opt/nulledroom
# Upload server.js and package.json here (scp, sftp, git clone, whatever)
# Then install deps:
npm install
# Start with PM2
pm2 start server.js --name nulledroom
pm2 save
pm2 startup # follow the printed command to make it survive reboots

3. Deploy frontend

bash
# Create web root
sudo mkdir -p /var/www/nulledroom
# Upload index.html here
sudo cp /path/to/index.html /var/www/nulledroom/index.html

4. Configure Nginx

bash
# Copy the nginx config
sudo cp /path/to/nulledroom.nginx.conf /etc/nginx/sites-available/nulledroom
# Enable it
sudo ln -s /etc/nginx/sites-available/nulledroom /etc/nginx/sites-enabled/nulledroom
# Remove default site if it's there
sudo rm -f /etc/nginx/sites-enabled/default
# Test config
sudo nginx -t
# Reload
sudo systemctl reload nginx

5. Get SSL cert

bash
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Follow the prompts. Certbot will automatically fill in the cert paths in your Nginx config.


6. Verify

bash
# Check PM2 status
pm2 status
# Check logs
pm2 logs nulledroom
# Check Nginx
sudo systemctl status nginx

Open your domain in a browser. You should see the NulledRoom boot screen.


Updating files

Update the frontend (index.html)

bash
# From your local machine, SCP the file up:
scp index.html user@yourserver.com:/var/www/nulledroom/index.html


No restart needed — Nginx serves the file directly.


Update the backend (server.js)

bash
# SCP to the server
scp server.js user@yourserver.com:/opt/nulledroom/server.js
# SSH into the server and restart PM2
ssh user@yourserver.com
pm2 restart nulledroom
pm2 logs nulledroom # verify it started clean

PM2 cheat sheet

bash
pm2 status # see running processes
pm2 restart nulledroom # restart after server.js update
pm2 logs nulledroom # tail logs
pm2 logs nulledroom --lines 100 # last 100 lines
pm2 stop nulledroom # stop (doesn't delete)
pm2 delete nulledroom # remove from PM2
pm2 save # save current process list for auto-start

Nginx cheat sheet

bash
sudo nginx -t # test config for syntax errors
sudo systemctl reload nginx # apply config changes (no downtime)
sudo systemctl restart nginx # full restart
sudo systemctl status nginx # check status

Notes

  • The server runs on port 3000 locally. Nginx proxies /ws to it over WebSocket and serves the frontend on 443.
  • Room IDs are 6 characters, alphanumeric (no ambiguous chars like 0/O, 1/I). Generated client-side.
  • Images are compressed to max 1920px on the longest side and run through quality reduction until under ~1.4MB before encryption. Canvas JPEG compression is used.
  • The uint8ToBase64 chunked encoding function is critical — using btoa(String.fromCharCode(...largeArray)) blows the call stack on large images. The chunked version processes 8192 bytes at a time.
  • Guests (non-creators) silently reconnect on WebSocket drop (up to 8 attempts, 2 second intervals). Hosts do not auto-reconnect — their disconnect is treated as intentional.
  • There is zero persistence. If the server process dies, all rooms are gone. That's by design.

License

MIT