One click. A unique link. Anyone with the link watches your screen live.
WebRTC peer-to-peer · No account · No plugins · No recordings stored.
Browser (Host) Signaling Server (Railway) Browser (Viewer)
│ │ │
│── POST /api/rooms ──────►│ │
│◄─ { roomId } ───────────│ │
│ │ │
│── WS: host:create ──────►│ │
│ │◄─── WS: viewer:join ───────────│
│◄── viewer:joined ───────│ │
│ │ │
│── signal:offer ─────────►│─── signal:offer ──────────────►│
│◄── signal:answer ────────│◄── signal:answer ──────────────│
│── signal:ice ────────────►│─── signal:ice ─────────────────►│
│◄── signal:ice ───────────│◄── signal:ice ──────────────────│
│ │ │
│◄══════════════ Direct WebRTC P2P stream ═══════════════►│
The signaling server only brokers the WebRTC handshake (offer/answer/ICE).
The actual video stream travels directly peer-to-peer — the server never sees your screen.
- Node.js 18+
- A modern browser (Chrome, Edge, Firefox, Safari 15+)
# From repo root
npm run install:all
# Or manually:
cd server && npm install
cd ../client && npm installServer:
cp server/.env.example server/.env
# Default values work for local dev — no edits neededClient:
cp client/.env.example client/.env
# Default values work for local dev — no edits neededOpen two terminal tabs:
# Terminal 1 — Signaling server (port 3001)
npm run dev:server
# Terminal 2 — React client (port 5173)
npm run dev:client- Open http://localhost:5173
- Click "Start Screen Share"
- Choose which screen/window/tab to share
- Copy the link from the bottom panel
- Open the link in another browser tab (or send to someone else)
- They see your screen live ✓
| Variable | Default | Description |
|---|---|---|
PORT |
3001 |
Port the signaling server listens on |
CLIENT_URL |
http://localhost:5173 |
Allowed CORS origin (your frontend URL) |
| Variable | Default | Description |
|---|---|---|
VITE_SERVER_URL |
http://localhost:3001 |
URL of the signaling server |
- Create a new project at railway.app
- Connect your GitHub repo (or drag-drop the
server/folder) - Set the Root Directory to
server - Railway auto-detects Node.js via
package.json - Add environment variables in Railway dashboard:
PORT=3001 CLIENT_URL=https://your-app.vercel.app - Deploy — Railway gives you a public URL like
https://screencast-server.up.railway.app
- Push the repo to GitHub
- Import the project at vercel.com
- Set Root Directory to
client - Set Build Command:
npm run build - Set Output Directory:
dist - Add environment variable:
VITE_SERVER_URL=https://screencast-server.up.railway.app - Deploy — Vercel gives you a URL like
https://screencast.vercel.app
Go back to Railway and update:
CLIENT_URL=https://screencast.vercel.app
Redeploy the server. Done ✓
- New Web Service → connect repo → Root:
server - Build command:
npm install - Start command:
node index.js - Add env vars:
PORT=10000,CLIENT_URL=https://your-app.vercel.app - Free tier has spin-down delay — Railway is recommended for lower latency
cd server
fly launch --name screencast-server
fly secrets set CLIENT_URL=https://your-app.vercel.app
fly deployscreenshare/
├── package.json # Root convenience scripts
│
├── server/
│ ├── index.js # Express + Socket.io signaling server
│ ├── package.json
│ ├── .env.example
│ ├── railway.toml # Railway deployment config
│ └── Procfile # Render/Heroku deployment
│
└── client/
├── index.html
├── vite.config.js
├── vercel.json # Vercel SPA routing
├── package.json
├── .env.example
└── src/
├── main.jsx
├── App.jsx # Router: / and /room/:roomId
├── index.css # Design system (CSS variables)
│
├── pages/
│ ├── HomePage.jsx # Landing page + "Start Screen Share"
│ ├── HomePage.css
│ ├── RoomPage.jsx # Host or Viewer depending on ?host=1
│ └── RoomPage.css
│
├── components/
│ ├── HostView.jsx # Screen preview + share link panel
│ ├── HostView.css
│ ├── ViewerView.jsx # Remote stream display + status UI
│ └── ViewerView.css
│
├── hooks/
│ ├── useHost.js # Host WebRTC logic (capture → peer → signal)
│ └── useViewer.js # Viewer WebRTC logic (signal → peer → display)
│
└── utils/
├── socket.js # Socket.io singleton
└── webrtc.js # RTCPeerConnection factory (ICE config)
- User clicks "Start Screen Share" →
POST /api/roomsgenerates a uniqueroomId - Browser calls
getDisplayMedia()to capture the screen - Socket connects and emits
host:createwith theroomId - For each viewer that joins, the host creates an
RTCPeerConnection, adds the screen track, creates an offer, and sends it via the signaling server - Once the viewer answers and ICE negotiation completes, video flows P2P
- Viewer opens
/room/:roomId(no?host=1param) - Socket connects and emits
viewer:join - Server notifies the host → host sends an offer
- Viewer creates
RTCPeerConnection, sets remote description, creates answer - ICE candidates are exchanged — P2P connection established
ontrackfires → stream is attached to<video>element- Auto-reconnect kicks in if the connection drops
- Room is created when host calls
host:create - Room is destroyed when host disconnects (all viewers notified)
- Multiple viewers are supported simultaneously (each gets its own peer connection)
| Browser | Screen Capture | Viewing |
|---|---|---|
| Chrome 72+ | ✓ | ✓ |
| Edge 79+ | ✓ | ✓ |
| Firefox 66+ | ✓ | ✓ |
| Safari 15.4+ | ✓ (limited) | ✓ |
Note:
getDisplayMediarequires HTTPS in production. Localhost is exempt.
This MVP uses in-memory room storage. For horizontal scaling:
- Replace the
roomsMap with Redis (e.g.ioredis) for shared state across instances - Add a TURN server (e.g. Twilio TURN, Cloudflare Calls) for clients behind strict NAT/firewalls
- The signaling server is stateless-friendly once Redis is added — scale with Railway replicas
MIT