Skip to content

Self-hosting the room

A built deck is a static site: copy dist/ to any host and it works, offline features and all. The live room needs a server. You can use deck.kadal.cc, or run your own.

The deck’s live client talks to live.api (/api by default, same origin) and joins the room named by live.room (the deck’s slug when unset). A room server answers:

Path What
GET {api}/rooms/{room}/ws the room, as a WebSocket. ?role=presenter asks for the presenter role; ?t= carries a Turnstile token when the room requires one
GET {api}/rooms/{room} a read-only snapshot: position and presence
GET {api}/decks/{room}/presenter { required, ok }: does the presenter view need a passcode, and is this window signed in
POST {api}/decks/{room}/presenter/login · /logout { passcode } sets or clears the presenter cookie
POST {api}/decks/{room}/beacon { viewer, dwell, enters } from each viewer, for time-per-slide analytics
GET {api}/decks/{room}/stats the stats page’s data (presenter only)
POST {api}/decks/{room}/share { h, v, click?, label? } → a short link to a slide

The messages on the WebSocket are typed in the engine — import them to build a server that can’t drift from the client:

import type { ClientMessage, ServerMessage, RoomState } from "@kadal/deck/protocol";

A client says hello with its role and a viewer ID; the server answers welcome with the full RoomState, then sends state patches and the transient events (nav, pointer, react, draw, term, confetti). Viewers’ messages that only a presenter may send — nav, poll:open, draw for everyone, session — must be dropped, and every viewer’s traffic throttled.

deck.kadal.cc runs on Cloudflare:

  • a Worker (Hono) routes the endpoints above and serves the decks;
  • one Durable Object per room, SQLite-backed, with WebSocket hibernation so an idle room costs nothing between messages; the room’s state is written to the object’s storage, so it survives eviction and a reload;
  • D1 keeps what outlives a room: passcode hashes, sessions, questions, poll results, time per slide, share links;
  • R2 holds the published files and the export archive;
  • Workers rate limiting on passcode attempts and writes.

Everything runs on Cloudflare’s free plan for a personal deck host.

The engine is licensed under the AGPL-3.0-or-later. In practice:

  • Presenting with it, publishing decks built with it, and running an unmodified engine trigger nothing.
  • If you run a modified engine as a network service, the people using that service must be able to get your modified source (AGPL §13).
  • A room server you write yourself against the protocol types is your own program.

If those terms don’t suit your product, a commercial licence removes them.