Markdown Converter
Agent skill for markdown-converter
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Sign in to like and favorite skills
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Mini Meet is a minimal 1:1 WebRTC video chat application. Users create a meeting via
/new, get a unique URL (/m/:id), and share it with one peer. The app uses Express for HTTP/WebSocket signaling and browser WebRTC APIs for peer-to-peer media streaming.
Start development server:
npm run dev # Or: node --run dev
This runs
scripts/dev.js which:
.local hostname via avahi (if available)Individual processes (if needed):
npm run watch-server # Server only with --watch flag npm run watch-build # Tailwind CSS only npm run build # One-time Tailwind CSS build
Production:
npm start # Runs src/server.js directly
Logging: Enable specific namespaces via
DEBUG env var:
DEBUG=mini-meet:* # All logs DEBUG=mini-meet:ws,mini-meet:beacon # Only WebSocket + client beacons DEBUG=mini-meet:ws:msg # Verbose WebSocket messages
src/server.js)Single-file server with these components:
HTTP/HTTPS Server: Express app that conditionally creates HTTP or HTTPS server based on
SSL_CERT_PATH and SSL_KEY_PATH env vars
Middleware Stack (order matters):
express.json() body parser (must come early for /log endpoint)req.logpublic//ph/static/* MUST come before /ph/*)/new, /m/:id, /, /turn, /logWebSocket Signaling Server:
/ws?roomId=xyzMap<roomId, Set<WebSocket>>room_full)ready, offer, answer, candidate, bye, leaveScoped Logger Pattern:
req.log = { http: createLogger('mini-meet:http', { ip, roomId }), turn: createLogger('mini-meet:turn', { ip }), beacon: createLogger('mini-meet:beacon', { ip }) };
All logs auto-include
ip=X.X.X.X roomId=xyz for easy filtering.
public/meeting.js)Single-page WebRTC client with these responsibilities:
Media Management:
startLocalMedia(): Requests camera/mic, displays in all [data-local-video] elementsswapCameraFacing())WebSocket Signaling:
connectWebSocket(): Connects to /ws?roomId=xyzwelcome, ready, offer, answer, candidate, bye, leave, room_fullisShuttingDown)WebRTC Peer Connection:
setupPeerConnection(reason): Creates new RTCPeerConnectionwelcome: Only recreate if PC doesn't exist or is unhealthybye: Only tear down if PC is actually dead (preserves healthy connections)leave: Always tear down (explicit peer exit)UI State Management:
Client Beacons:
beacon(event, context): Sends to /log via navigator.sendBeacon()window.posthog.capture()application/json type (plain objects send as text/plain)src/views/)Server-side HTML generation using
:nanohtml
layout.html.js: Base HTML structure, injects Rollbar + PostHog snippetsmeeting.html.js: Meeting page with responsive mobile/desktop layoutsindex.html.js: Landing pagerollbar.html.js, posthog.html.js: Script injection snippetsImportant: Views use
html tagged template literals from nanohtml. Server-side renders static HTML with embedded JS variables (e.g., window.__IS_MOBILE__ = ${isMobile}).
Why: Bypass ad blockers by making PostHog requests appear same-origin.
Server proxy setup (
):src/server.js
// Order matters! Static MUST come before general /ph proxy app.use('/ph/static', createProxyMiddleware({ target: 'https://eu-assets.i.posthog.com', changeOrigin: true, // Sets Host header pathRewrite: { '^/': '/static/' } // Express strips /ph/static, so rewrite / to /static/ })); app.use('/ph', createProxyMiddleware({ target: 'https://eu.i.posthog.com', changeOrigin: true, pathRewrite: { '^/ph': '' } }));
Client setup:
src/views/posthog.html.jsapi_host: '/ph' (routes all requests through proxy)/log and PostHogServer (
endpoint):/turn
username = timestamp:random_tag, credential = HMAC-SHA1(username, TURN_SECRET){ iceServers: [...], ttl } for clientClient:
/turn during peer connection setuptransport=udp stripped)Mobile devices switch between two layouts:
Layout switching happens in
applyMobileLandscapeLayout() triggered by:
resize eventorientationchange eventorientationQuery.matches media query changeMobile overlay:
overlayPinchState!w-24, !w-40) before setting inline widthDesktop overlay:
desktopOverlayDragStatedetectUnsupportedBrowser() checks for:
navigator.mediaDevices.getUserMediaIf detected, shows modal with "Share Link" button (uses
navigator.share if available).
Uses
pagehide event (not beforeunload) for iOS Safari compatibility:
window.addEventListener('pagehide', () => { markShuttingDown(); try { send('bye'); } catch (_) {} });
Required for HTTPS (dev/production):
SSL_CERT_PATH, SSL_KEY_PATH: Paths to cert/key filesOptional TURN:
TURN_URLS: Comma-separated TURN/TURNS URLsTURN_SECRET: Shared secret (matches coturn config)TURN_TTL: Credential lifetime in seconds (default 900)Optional Analytics:
POSTHOG_API_KEY: PostHog project API key (public, starts with phc_)POSTHOG_API_HOST: Defaults to /ph (proxy endpoint)ROLLBAR_SERVER_ACCESS_TOKEN, ROLLBAR_CLIENT_ACCESS_TOKENROLLBAR_ENVIRONMENT: e.g., development, productionServer:
PORT: Server port (default 3003)DEBUG: Debug namespace filter (e.g., mini-meet:*)Use scoped loggers attached to
req or create via createLogger():
req.log.http('some message'); // Auto-includes ip and roomId const wsLogger = createLogger('mini-meet:ws', { ip, roomId }); wsLogger('connection established');
Add beacon call + PostHog will auto-receive it:
beacon('event_name', { key: 'value' }); // Sends to /log + PostHog (if configured)
Be careful when editing
welcome and bye handlers. Current logic:
leave message always tears down (explicit peer departure)Views use
nanohtml tagged template literals. To add server-side variables:
html`<script>window.__MY_VAR__ = ${myVar};</script>`
Access in client JS:
const myVar = window.__MY_VAR__;
Dokku (recommended): See README.md "Deploy on Dokku" section.
Docker: Use provided
Dockerfile. Set env vars via docker-compose or runtime flags.
TURN server: Self-host with coturn (see
coturn/docker-compose.yml) or use managed provider (Twilio, Cloudflare Calls).