nullroom.io is a zero-trace, ephemeral peer-to-peer messaging app built with Rails, Stimulus, WebRTC, and Web Crypto.
Two people share a link, establish a direct encrypted data channel, and exchange messages without message persistence on the server.
- No accounts, no identities: users create/join rooms by URL only.
- Ephemeral rooms: room state is stored in Redis with a TTL (15 minutes by default).
- 2-person rooms: the signaling channel enforces max capacity.
- Client-side encryption: AES-GCM keys are generated in-browser and stored in the URL fragment (
#...), which is not sent to the server. - P2P messaging: messages are sent over WebRTC DataChannels after signaling.
- User clicks Create Room on
/. - Frontend sends
POST /rooms; server creates room UUID in Redis and returns JSON withroom_idand TURNice_servers. - Browser generates AES key locally and redirects to
/rooms/:id#<key>. - Room page subscribes to
RoomsChannelfor WebRTC signaling (offer/answer/ICE candidate relay). - Once peer connection is established, encrypted messages move directly via DataChannel.
- If a peer leaves, the other peer gets immediate termination UX and message UI is scrubbed.
- Encryption key is generated in browser (
Web Crypto API) and passed in URL fragment only. - Rails receives room IDs, not encryption fragments.
- ActionCable relays signaling payloads and does not decrypt chat content.
- WebRTC handles encrypted transport between peers.
- Application-level message encryption uses AES-GCM in
app/javascript/modules/encryption.js. - TURN credentials are fetched server-side via
CloudflareTurnServiceand passed to clients.
- Redis keys:
room:<uuid>(room existence / TTL)room:<uuid>:count(participant count)
- Room/channel state expires automatically based on TTL.
- Landing page with one-click room creation.
- JSON room creation endpoint (
POST /rooms). - Room view (
GET /rooms/:id) with:- signaling status indicator,
- room timer,
- share-link + copy UX,
- message input and encrypted P2P send/receive,
- room-terminated modal when peer disconnects.
- ActionCable signaling channel with:
- room existence checks,
- max 2 participants,
- signal relay and peer-left notifications.
- Health endpoint at
/upwith Redis write check and basic rate limiting.
These are planned but not fully implemented yet:
- Blind JWT token system for pro room features (
exp+ feature flags only, no identity binding). - Optional longer room TTL via valid token (e.g., 24h).
- Hardened CSP policy for production signaling/TURN hosts.
- Additional deployment-level no-log hardening and operational refinements.
- Ruby on Rails 8
- ActionCable
- Redis
- Stimulus + Turbo + Importmap
- Tailwind CSS
- WebRTC (
RTCPeerConnection+ DataChannel) - Web Crypto API (AES-GCM)
- SQLite (default app DB)
By default, rooms are destroyed immediately when any peer leaves, preventing rejoins via browser history.
To allow peers to rejoin (room stays active until TTL expires), edit config/initializers/nullroom.rb:
DESTROY_ROOM_ON_PEER_LEAVE = false # Allow rejoin via browser historyThe file transfer gate is configurable via environment variable (bytes):
NULLROOM_FILE_TRANSFER_SIZE_LIMIT_BYTES=16777216Default is 16 MiB (16777216 bytes). The same limit is enforced server-side and reflected client-side in the room UI.
GET /→ room landing pagePOST /rooms→ create room JSONGET /rooms/:id→ room UIGET /privacy→ privacy pageGET /up→ app + Redis health checkGET /cable→ ActionCable endpoint
Please read our security policy and disclosure process in SECURITY.md.
This project is licensed under the MIT License. See LICENSE.