New Project

Minimal realtime starter built with Nitro v3, React, shadcn/ui (on Base UI primitives), and the Vercel Functions WebSockets beta. Move your cursor and everyone in the room sees it live — presence, cursors, and emoji reactions over a single WebSocket connection. No auth, no client SDK.
No environment variables and no external services — the realtime layer runs entirely on native WebSocket pub/sub (see How it works).
npx giget@latest gh:vercel/examples/websockets/nitro my-realtime-appcd my-realtime-apppnpm installpnpm dev
Open http://localhost:3000 in two browser tabs (or share the URL) to see live cursors, presence, and reactions.
A Vercel Function can accept a WebSocket upgrade and keep a bidirectional connection open. Nitro v3 ships native WebSocket support powered by crossws, enabled with a single flag:
// vite.config.tsnitro({ features: { websocket: true } })
The headline: one transport, every environment. The same defineWebSocketHandler (server/api/ws.ts) at /api/ws powers local dev and production — locally through Nitro's dev server, on Vercel through the preset's crossws/adapters/vercel bridge, which hands the handler the runtime's socket upgrade. There's no Vercel-specific code path and no experimental_upgradeWebSocket bridge to maintain.
All room logic lives in the handler (server/api/ws.ts) itself.
Each connection subscribes to a single room topic. Cursor moves, reactions, and join/leave events are broadcast with crossws's native peer.publish (server/api/ws.ts) — no external store and no client SDK. The connected roster is held in memory and replayed to each client in the welcome frame on connect, so a reconnect rebuilds it from scratch.
app/ # React SPA (Vite)├── App.tsx # page composition├── hooks/use-realtime.ts # connection, reconnect, heartbeat├── components/ # LiveCanvas, Cursor, PresenceBar, HeroBackdrop└── components/ui/ # shadcn/ui primitives (Base UI)shared/└── types/realtime.ts # ClientMessage / ServerMessage / Peer — the wire protocolserver/├── api/ws.ts # /api/ws — native WebSocket handler + room pub/sub└── utils/└── identity.ts # anonymous identity (name + color) per connection
The frontend and the Nitro server are wired together by the nitro/vite plugin: index.html is served as the SPA renderer for all unmatched routes, and server/ routes (like /api/ws) are matched first.
WebSocket connections close when a Vercel Function reaches its maximum duration. The client reconnects with exponential backoff and reloads the roster from the welcome frame on each new connection — see use-realtime.ts.
A lightweight heartbeat (ping/pong) runs over the same socket so the client can detect a half-open connection (a missed pong) and force a reconnect. On disconnect, the server's close/error handlers publish a leave frame so the peer drops from everyone's roster.
server/utils/identity.ts for your authenticated user.ClientMessage / ServerMessage in shared/types/realtime.ts, then handle it in the handler server/api/ws.ts. The types are shared, so the client and server stay in sync.pnpm dlx shadcn@latest init -b base to generate them.Published under the MIT license.
