A real-time screen sharing + file transfer + live chat platform. Zero accounts, zero plugins β just a 6-char code.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Host shares screen β Viewers join with code β Everyone can chat β
β Anyone can send files β 24h links + 6-char codes β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Feature | Details |
|---|---|
| π₯οΈ Screen sharing | WebRTC P2P, host β multiple viewers, fullscreen on both ends |
| π€ File send | Upload up to 50MB β get a 6-char code + 24h download link |
| π₯ File receive | Enter code or open direct link β instant download |
| π¬ Live chat | Real-time WebSocket chat with avatars, typing indicators |
| π In-chat files | Share files inside the chat; auto-uploaded and coded |
| π₯ Presence | Floating emoji avatars, arrow-key movable |
| π Shareable links | /watch/ROOMCODE and /download/FILECODE deep links |
git clone <your-repo>
cd vault
# Backend
cd server && npm install
# Frontend
cd ../client && npm install# client/.env (copy from .env.example)
VITE_WS_URL=ws://localhost:3001
VITE_API_URL=http://localhost:3001Terminal 1 β backend:
cd server
node server.js
# π Vault Server on ws://localhost:3001Terminal 2 β frontend:
cd client
npm run dev
# β http://localhost:5173That's it. Open two browser tabs, share a room code, and it works.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Browser (Host) β
β β
β getDisplayMedia() β RTCPeerConnection β ICE/STUN β
β WebSocket βββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββΌβββ
β Vault Signaling Server β
β Node.js Β· ws Β· http β
β β
β Sessions Map: roomCode β { host, β
β viewers, chat } β
β β
β Files Map: code β { data, expiresAt } β
β (auto-purged after 24h) β
ββββββββββββββββββββββββββββββββββββββ¬βββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Browser (Viewer) β
β β
β WebSocket ββββββββββββββββββββββββββββββββββββββββββββββ
β Offer β Answer β ICE β RTCPeerConnection β
β video.srcObject = remoteStream β plays stream β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Host Server Viewer
β β β
βββ host (name) βββββββββΊβ β
βββ room-created βββββββββ β
β βββ viewer (code, name) ββ
βββ viewer-joined ββββββββββ room-joined ββββββββββΊβ
βββ offer βββββββββββββββΊβββ offer ββββββββββββββββΊβ
β βββ answer ββββββββββββββββ
βββ answer βββββββββββββββ β
βββ ice βββββββββββββββββΊβββ ice ββββββββββββββββββΊβ
βββ ice ββββββββββββββββββββ ice βββββββββββββββββββ
β β β
ββββββββββββ P2P video stream (direct) ββββββββββββ
Sender Server Receiver
β β β
βββ POST /api/files ββββββΊβ β
β (base64 body) β stores in fileStore β
βββ { code, expiresAt } ββ β
β β β
β shares code/link β β
β βββ GET /api/files/:code/meta
β βββ { fileName, size } βββΊβ
β βββ GET /api/files/:code β
β βββ binary stream βββββββββΊβ
β β (auto-deleted 24h) β
vault/
βββ server/
β βββ server.js # WebSocket signaling + REST API
β βββ package.json
β
βββ client/
βββ index.html
βββ vite.config.js
βββ .env.example
βββ src/
βββ main.jsx
βββ App.jsx # Router + state machine
βββ hooks/
β βββ useWebRTC.js # RTCPeerConnection logic
β βββ useWebSocket.js # WS connection + reconnect
βββ components/
β βββ Lobby.jsx # Landing/home page
β βββ HostRoom.jsx # Screen share host view
β βββ ViewerRoom.jsx # Stream viewer view
β βββ ChatPanel.jsx # Slide-in chat drawer
β βββ VideoPlayer.jsx # Video element + controls
β βββ Avatar.jsx # Static + floating avatars
β βββ FileSend.jsx # Upload β get code/link
β βββ FileDownload.jsx # Enter code β download
β βββ WatchPage.jsx # Join stream by code/link
βββ styles/
β βββ global.css
βββ utils/
βββ avatar.js # Emoji avatars, helpers
| URL | Description |
|---|---|
/ |
Home / lobby |
/send |
Upload a file, get 24h code + link |
/download |
Enter a file code to download |
/download/ABC123 |
Direct download link (auto-fetches file) |
/watch |
Enter a room code to join a stream |
/watch/ABC123 |
Direct stream join link |
/share/ABC123 |
Host view with shareable link (auto-set) |
Upload a file as base64. Returns a 6-char code and expiry.
// Request
{
"fileName": "report.pdf",
"fileSize": 102400,
"fileType": "application/pdf",
"fileData": "data:application/pdf;base64,..."
}
// Response
{
"code": "AB3K9X",
"expiresAt": 1714000000000
}Check file info without downloading.
{
"fileName": "report.pdf",
"fileSize": 102400,
"fileType": "application/pdf",
"uploadedAt": 1713913600000,
"expiresAt": 1714000000000,
"downloads": 3
}Download the file (binary, triggers browser download).
Check if a room is live.
{
"roomCode": "66F2WW",
"hostName": "Bold Crane",
"viewerCount": 2,
"createdAt": 1713913600000
}Server health check.
| Direction | Type | Payload |
|---|---|---|
| ClientβServer | host |
{ name, avatar } |
| ClientβServer | viewer |
{ roomCode, name, avatar } |
| ClientβServer | offer |
{ sdp, targetId } |
| ClientβServer | answer |
{ sdp } |
| ClientβServer | ice |
{ candidate, targetId? } |
| ClientβServer | chat |
{ text } |
| ClientβServer | typing |
{ isTyping } |
| ClientβServer | file-share |
{ fileName, fileCode, fileUrl, ... } |
| ServerβClient | room-created |
{ roomCode, clientId } |
| ServerβClient | room-joined |
{ roomCode, clientId, hostName, chatHistory } |
| ServerβClient | viewer-joined |
{ viewerId, name, viewerCount } |
| ServerβClient | viewer-left |
{ viewerId, viewerCount } |
| ServerβClient | offer |
{ sdp } |
| ServerβClient | answer |
{ sdp, viewerId } |
| ServerβClient | ice |
{ candidate } |
| ServerβClient | chat |
{ id, name, text, timestamp } |
| ServerβClient | typing |
{ name, isTyping } |
| ServerβClient | host-left |
{} |
# server
PORT=3001
# client/.env.production
VITE_WS_URL=wss://your-domain.com/ws
VITE_API_URL=https://your-domain.comcd client && npm run build
# Output: client/dist/server {
listen 443 ssl;
server_name your-domain.com;
# SPA
root /var/www/vault/client/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html; # SPA fallback
}
# REST API
location /api {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
}
# WebSocket signaling
location /ws {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
}Note: With this nginx config, update
VITE_WS_URL=wss://your-domain.com/wsand the server must listen on/wspath. Or just use a subdomain for the WS server directly.
npm install -g pm2
cd server
pm2 start server.js --name vault-server
pm2 save
pm2 startupWebRTC works great on the same network or with STUN. For users behind strict firewalls/NATs, add a TURN server.
// In client/src/hooks/useWebRTC.js, add to ICE_SERVERS:
{
urls: 'turn:your-turn-server.com:3478',
username: 'vault',
credential: 'your-secret',
}Free TURN options: Metered.ca Β· Xirsys
- File storage is in-memory. A server restart loses all files. For production, store files in S3/R2 and save the metadata in Redis.
- Max file size is 50MB (base64 inflates ~33%, so actual payload is ~67MB). Configurable in
server.js. - Chat history keeps last 200 messages per room in memory.
- STUN only by default. Add TURN for cross-NAT production deployments.
- No E2E encryption on the server API (files rest in memory as base64). WebRTC media is DTLS-encrypted by the spec.
MIT