Skip to content
michidkPublic

About

Simple peer-to-peer screen sharing with WebRTC and PeerJS

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

miseshare

A deliberately small, open-source screen and file sharing service. Create a room, send a link, and let several participants share at once—no accounts, installs, or persisted room history.

miseshare landing page

Live demo: miseshare.vercel.app

Related project

If a connection fails, use the in-room connection check or icecheck (live tool) to isolate signaling, ICE candidate, data-channel, and media-path problems between two browsers.

Native WebRTC video tracks carry the 720p, 1080p, and 60 fps screen-sharing presets over encrypted browser connections. A configured TURN server can relay that encrypted traffic when a direct path is unavailable. A separate lossless text mode sends pixel-exact tile deltas over WebRTC data channels. File drops use a dedicated reliable data channel, begin only after each recipient accepts, and never upload file contents to the service. A small REST API stores temporary room admission and WebRTC signaling messages in PostgreSQL; it never receives screen, chat, audio, or file data.

The application is authored in strict TypeScript on Bun. TanStack Start and React 19 provide server rendering and file-based routing, Nitro serves the production app and API handlers, and shadcn's Base UI primitives sit underneath the custom Miseshare interface. Drizzle ORM owns the PostgreSQL schema and migrations.

Run it locally

bun install
bun run db:up
export DATABASE_URL='postgresql://miseshare:[email protected]:54329/miseshare'
export ADMIN_PASSWORD='choose-a-strong-local-password'
export ADMIN_SESSION_SECRET='generate-at-least-32-random-bytes'
bun run db:migrate
bun run dev

Open http://localhost:3000. Screen capture works on localhost; a deployed instance must use HTTPS.

Local development uses the PostgreSQL service in compose.yaml, bound to loopback on port 54329. DATABASE_URL is mandatory in every environment; there is no in-memory fallback. The schema is defined with Drizzle ORM in src/room-api/internal/schema.ts, with generated migrations tracked in drizzle/. The server applies pending committed migrations before accepting requests; a PostgreSQL advisory lock serializes concurrent serverless cold starts.

After changing the schema, generate and validate a migration before applying it:

bun run db:generate
bun run db:check
bun run db:migrate

Deploy to Vercel

Provision a Neon PostgreSQL database from the Vercel Marketplace and connect it to the project. Hosted production builds run committed Drizzle migrations under a PostgreSQL advisory lock before compiling and promoting the deployment. Preview and local Vercel builds skip production migrations. Keep schema changes backward-compatible with the currently deployed application so the migration and code promotion remain safe during rolling deployments. bun run build selects Nitro's Vercel preset when Vercel sets its build environment; outside Vercel it emits the Bun production server at .output/server/index.mjs for bun run start.

bunx vercel@latest --prod

Vercel serves the Nitro output and public assets from its CDN and runs the room REST API as stateless functions. Every invocation reads the same PostgreSQL room and signaling tables, so participants do not need to reach the same function instance. DATABASE_URL is mandatory on Vercel; the server fails fast instead of silently creating instance-local rooms.

Configuration

Variable Default Purpose
PORT 3000 HTTP port
HOST 0.0.0.0 HTTP bind address
BASE_PATH (empty) Optional URL prefix, such as /previews/miseshare
MAX_PARTICIPANTS 12 Deployment ceiling for total room participants, including the host (2–12)
DATABASE_URL (required) PostgreSQL connection string; use Docker locally and Neon in production
ADMIN_PASSWORD (required) Password for the read-only /admin database dashboard
ADMIN_SESSION_SECRET (required) Independent random secret used to sign eight-hour admin sessions; use at least 32 bytes
SECURE_COOKIES production/Vercel: true; otherwise false Require HTTPS for the admin session cookie
TRUST_PROXY false (Vercel configures one trusted hop) Trust proxy-derived client IPs for rate limits; enable only behind a trusted proxy
RATE_LIMIT_ENABLED true Enforce PostgreSQL-backed create, join, signal, and admin-login limits across instances
REQUEST_LOGGING production/Vercel: true; otherwise false Emit structured request ID, status, path, and duration logs; server errors are always logged
OBSERVABILITY_ENABLED production/Vercel: true; otherwise false Emit structured room lifecycle, signaling-failure, peer-state, and direct/TURN route events with HMAC-anonymized identifiers
VITE_HEAD_HTML (empty) Trusted markup injected verbatim into the app <head> at server startup
EMOTES_ENABLED true Load the global emote catalog and serve assets through the same-origin image proxy
STUN_URLS stun:stun.l.google.com:19302 Comma-separated STUN URLs; non-STUN entries are ignored
TURN_URLS (empty) Comma-separated turn: or turns: URLs for a coturn-compatible relay
TURN_SHARED_SECRET (required with TURN_URLS) coturn REST authentication secret; never sent to browsers
TURN_TTL_SECONDS 3600 Lifetime of generated TURN credentials, from 60 to 86400 seconds

When STUN_URLS contains multiple servers, browsers may query them concurrently; WebRTC does not guarantee a strictly sequential failover order.

VITE_HEAD_HTML accepts complete tags, such as a Meta Pixel <script> or a site-verification <meta> tag. Treat it as trusted executable configuration: never populate it from user input or another untrusted source. When configured, the app page's content security policy adds the request nonce to injected script and style tags and permits HTTPS origins needed by third-party analytics. Inline event-handler attributes are intentionally unsupported. Leave it unset to inject nothing and retain the strict default policy.

For reliable connectivity across restrictive NATs and corporate networks, deploy coturn with its REST API shared-secret mechanism, then set TURN_URLS and TURN_SHARED_SECRET. The app creates short-lived HMAC credentials per /config request and refreshes them before ICE recovery; the shared secret never leaves the server. A normal HTTP reverse proxy does not replace TURN because it cannot relay WebRTC media.

Global Twitch, BetterTTV, FrankerFaceZ, and 7TV emotes are available without provider credentials. Native Twitch emote metadata comes from the public Adam C Younis aggregate catalog. Provider URLs are never exposed to the browser. The server validates provider hosts and image types, applies size and timeout limits, caches the result, and serves it from /emotes/assets/:id. Disable this optional outbound provider access with EMOTES_ENABLED=false.

How it works

  • The REST API creates a random room code, hashes optional room passwords with scrypt, and atomically enforces the deployment-wide participant capacity.
  • Room, participant, and signaling records are short-lived. Host heartbeats extend the room while the tab is open; stale participants and signaling messages expire automatically.
  • Each browser receives an opaque participant token. The API hashes that token before storage and derives the signaling sender from the authenticated request rather than trusting client-provided identity.
  • Browsers exchange SDP descriptions and ICE candidates through authenticated REST mailboxes. Once negotiation completes, native RTCPeerConnection data channels and media tracks communicate over encrypted direct or TURN-relayed paths.
  • Room participants can drop files up to 256 MB to every connected peer. Each recipient accepts independently, receives ordered 32 KiB chunks with backpressure, and downloads the reconstructed browser-local blob; file bytes never touch the REST API or database.
  • Every participant can publish independently, so several screens can be live at once. Each publisher opens an encrypted peer connection to every other room participant.
  • The first media codec is text-lossless-v1: it captures native RGBA pixels, compares 128 px tiles exactly, DEFLATE-compresses only changed tiles, and sends a periodic repair keyframe every 15 seconds. Frames are split into 48 KiB messages; a dropped or invalid delta triggers an immediate keyframe request before rendering resumes.
  • Stream audio is kept on a separate native WebRTC media track. Participants can share microphone input with or without an active screen; when screen audio is also active, the browser mixes both sources into the same outgoing track. Each source remains independently controllable, and every receiver can mute each incoming stream.
  • Codec settings and stream ownership are room metadata. Cards show the host, current codec settings, and audio state without coupling the UI to the encoder implementation.
  • A host-coordinated activity log records joins, leaves, stream starts/stops, audio changes, and settings changes alongside chat. Only the latest 100 entries live in the host's browser memory.
  • src/room-api/index.ts is the server boundary for room admission and signaling storage. Its Drizzle/PostgreSQL implementation remains internal.
  • src/signaling/index.ts is the browser boundary for REST room lifecycle, heartbeat, and signaling mailboxes.
  • src/rtc/index.ts owns native WebRTC negotiation and recovery, fixed control/screen/diagnostics/drop channels, MessagePack serialization, and audio/video transceivers. src/drop/index.ts owns file offers, acceptance, chunking, backpressure, cancellation, and reconstruction.
  • src/media/index.ts owns encoder, renderer, backpressure, and presentation cleanup. src/room/index.ts owns validated host/viewer messages and UI session state.
  • src/chat-ui/index.ts, src/emotes/index.ts, and src/ice-config/index.ts isolate browser chat behavior, the same-origin emote proxy, and ephemeral ICE configuration behind small public interfaces.
  • There is no recording, analytics, account system, or backend media-processing path.

The full-mesh layout is ideal for small groups. Its bandwidth and connection count grow with every participant, so use an SFU for large audiences rather than raising the 12-person ceiling.

Operations and verification

GET /health/live checks the process; GET /health/ready (and the compatibility endpoint GET /health) checks PostgreSQL readiness. Successful requests include an X-Request-Id header. Production logs include structured request timing plus anonymized room lifecycle and WebRTC connectivity events; turnUsed: true identifies relay usage without logging room codes or participant IDs. The /admin dashboard paginates in PostgreSQL, preserves its last successful snapshot during transient failures, and intentionally redacts password hashes, participant tokens, and signaling payloads.

Run the normal verification suite with bun run verify. To include the two-browser room flow and mobile-layout checks in Chromium and Firefox, install the browsers once and run the full suite:

bunx playwright install chromium firefox
bun run verify:full

Browser screenshots, videos, traces, and reports stay under .playwright/. CI also checks production dependency advisories and verifies that TanStack Router's generated route tree is current.

License

MIT

About

Simple peer-to-peer screen sharing with WebRTC and PeerJS

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages