A private, real-time media sharing and video conferencing platform built on a peer-to-peer WebRTC mesh. Rooms are ephemeral — no sign-up, no persistent storage, no media bytes touch the server.
- Video & audio conferencing — full P2P WebRTC mesh (up to ~5 participants)
- Screen sharing — replace your video feed with your screen in one click
- Background blur — live canvas-based blur pipeline; toggled instantly without re-acquiring the camera
- Speaking detection — green glow border highlights whoever is talking
- Pre-join preview — adjust camera, mic, blur, and mute preferences before entering the room
- Room isolation — each room has its own ID; no room persists after everyone leaves
- Password-protected rooms — optional room passwords with inline prompts in the lobby
- Private / public rooms — admins can hide their room from the public lobby at any time
- Duplicate session detection — opening the same session in a second tab shows a takeover prompt
- Mute participants — admin can mute any participant or mute everyone at once
- Disable camera — admin can turn off a participant's camera (user must re-enable it themselves)
- Kick participants — admin can remove anyone from the room
- Privacy guarantee — the host can never force-unmute or force-re-enable a camera; only the participant can lift those restrictions
- Live chat — room broadcast messages visible to everyone
- Private messages — DM any participant directly; toast notification on receipt
- Presence system — see who is online, which room they are in, and their mute/camera status
- Raise hand — signal you want to speak; count badge on the hand button shows how many hands are up; badge also shows on your own video tile
- Emoji reactions — send floating emoji reactions visible to everyone
- Polls — any participant can create a poll; creator or host can close it; live vote counts update in real time
- Q&A — submit questions, upvote others', and mark them answered; activity dot on the Q&A button when there are unanswered questions
- Activity dots — emerald indicator dot on Polls and Q&A footer buttons when there is unread activity
- Notifications — toasts for new polls, new questions, and incoming private messages
- Sound effects — subtle audio cues for join, message, and click events
- PWA — installable on mobile and desktop; works offline for the app shell
- Dark / light mode — system preference respected; togglable in-room
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript, Vite 6 |
| Styling | Tailwind CSS v4, motion (Framer Motion v12) |
| Icons | Lucide React |
| Real-time | Socket.IO v4 (client + server) |
| Video / Audio | WebRTC (browser-native, P2P mesh) |
| Backend | Node.js, Express 4 |
| Dev server | tsx (no compile step in development) |
| PWA | vite-plugin-pwa |
lantern/
├── shared/
│ ├── types.ts # Domain models & socket payload types (shared by server + client)
│ └── events.ts # All socket event name constants (shared by server + client)
│
├── server/ # Node.js / Express / Socket.IO backend
│ ├── index.ts # Entry point — HTTP server + Vite middleware wiring
│ ├── config.ts # PORT, NODE_ENV, rate-limit tuning
│ ├── repositories/
│ │ ├── userRepository.ts # In-memory user store (swap for DB adapter here)
│ │ └── roomRepository.ts # In-memory room metadata store
│ ├── services/
│ │ ├── presenceService.ts # Builds + broadcasts the presence snapshot
│ │ └── roomService.ts # Leave-room logic + room cleanup
│ ├── lib/
│ │ └── rateLimiter.ts # Per-socket rolling-window rate limiter
│ └── socket/
│ ├── index.ts # Wires all handlers onto the io instance
│ └── handlers/
│ ├── userHandler.ts # set-name, toggle-room-visibility
│ ├── roomHandler.ts # join-room, leave-room, privacy, host controls, disconnect
│ ├── chatHandler.ts # send-message, send-private-message
│ ├── engagementHandler.ts # raise-hand, reactions, polls, Q&A
│ └── webrtcHandler.ts # offer / answer / ice-candidate relay
│
└── src/ # React frontend
├── main.tsx # React root — wraps App in AppProvider
├── App.tsx # Thin orchestrator: hooks → pages router
├── context/
│ └── AppContext.tsx # Global state: step, userName, presence, notifications
├── hooks/
│ ├── useMedia.ts # Camera/mic/screen capture, blur pipeline, track toggles
│ ├── useWebRTC.ts # Peer connection lifecycle, offer/answer/ICE
│ ├── useRoom.ts # Chat message state + send helpers
│ ├── useEngagement.ts # Raise hand, reactions, polls, Q&A state + socket events
│ └── useNotifications.ts # Toast queue with auto-dismiss
├── pages/
│ ├── NameEntryPage.tsx # Step 1 — set display name
│ ├── PreJoinPage.tsx # Step 2 — camera/mic preview + device settings
│ ├── LobbyPage.tsx # Step 3 — create / join rooms, browse online users
│ └── RoomPage.tsx # Step 4 — video grid, controls, sidebar
├── components/
│ ├── VideoPlayer.tsx # Video tile with speaking detection, fullscreen, zoom/pan
│ ├── Chat.tsx # Message list + input (supports private DMs)
│ ├── Sidebar.tsx # Tabbed panel: Chat / Room users / All online
│ ├── EngagementToolbar.tsx # Raise-hand button (with count badge) + emoji picker
│ ├── ReactionsOverlay.tsx # Floating emoji reactions layer
│ ├── PollPanel.tsx # Create polls, vote, view results
│ ├── QAPanel.tsx # Submit questions, upvote, mark answered
│ └── ui/
│ ├── NotificationToast.tsx # Fixed top-right toast stack
│ └── MediaSettingsModal.tsx # Camera / mic picker + join preferences
└── lib/
├── socket.ts # Socket.IO singleton
├── sounds.ts # UI sound effect helper
├── constants.ts # ICE server config
└── utils.ts # cn() Tailwind class merge helper
- Node.js >= 18
- A browser with WebRTC support (Chrome, Firefox, Edge, Safari 15+)
git clone <repo-url>
cd lantern
npm installCopy the example file and edit as needed:
cp .env.example .env.local| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Port the server listens on |
NODE_ENV |
development |
Set to production to serve the Vite build |
MSG_RATE_MAX |
10 |
Max chat messages per socket per window |
MSG_RATE_WINDOW_MS |
5000 |
Rolling window duration (ms) for the message rate limit |
MAX_MSG_LENGTH |
500 |
Hard cap on chat message length (characters) |
JOIN_COOLDOWN_MS |
3000 |
Min ms between consecutive join-room attempts per socket |
GEMINI_API_KEY |
— | Google Gemini key (reserved for future AI features) |
npm run devStarts the Express + Socket.IO server on http://localhost:3000. Vite runs in middleware mode so HMR works out of the box.
npm run build # Vite bundles the frontend into dist/
NODE_ENV=production npm run dev # Serves dist/ via ExpressBrowser A Server Browser B
|-- set-name -----------> | |
|-- join-room ----------> | <-- join-room ------------ |
|<-- user-joined -------- | ---- user-joined --------> |
WebRTC Signaling (server is a relay only — no media bytes):
|-- offer -------------> | ---- offer ---------------> |
|<-- answer ------------ | <--- answer --------------- |
|-- ice-candidate -------> | ---- ice-candidate -------> |
After ICE: A <============== P2P Media ==============> B
- The server only relays WebRTC signaling messages (offer / answer / ICE candidates).
- All audio and video travel directly between browsers via DTLS-SRTP encrypted P2P streams.
- Chat messages are routed through the server via Socket.IO (not stored).
- A user picks a Room ID and clicks Create & Join — the server registers the room and marks that socket as admin.
- Other users Join with the same ID (and optional password).
- Each new joiner triggers a WebRTC offer from every existing participant, growing the mesh.
- When the admin leaves, the server broadcasts
room-closedand all peers return to the lobby. - If a non-admin leaves, only their peer connections are torn down.
| Action | How |
|---|---|
| Fullscreen a video tile | Click the expand icon (hover to reveal) |
| Zoom / pan in fullscreen | Scroll wheel to zoom, drag to pan |
| Send a private message | Click the DM icon next to a user in the Room or All tab |
| Mute yourself | Mic button in footer |
| Turn off camera | Video button in footer |
| Share your screen | Share button in footer |
| Toggle background blur | Blur BG button in footer (disabled when camera is off) |
| Raise / lower your hand | Hand button in footer; count badge shows total raised hands |
| Send an emoji reaction | Smile button → picker in footer |
| Create a poll | Polls panel → New Poll (any participant can create) |
| Close a poll | Polls panel → Close Poll (creator or host only) |
| Submit / upvote a question | Q&A panel → type a question or click the upvote arrow |
| Open Host Controls | Shield icon in header (admin only) |
All state lives in two in-memory Maps. Swap them for a SQLite (or other DB) adapter without touching any service or handler code:
server/repositories/userRepository.ts— replaceMapoperations with DB queriesserver/repositories/roomRepository.ts— same pattern
better-sqlite3 is already installed.
@google/genai is already installed. Set GEMINI_API_KEY in .env.local and import the client anywhere in the server layer.
| Branch | Description |
|---|---|
main |
Stable baseline |
refactor/separate-backend-frontend |
Layered architecture (current) |
MIT