Beginner-first production tutorial (Ubuntu + Docker Compose)

Self-host the LetsChat backend

LetsChat is not a single server. It is a small backend stack: chat state, auth, voice signaling, file storage, and startup discovery. This guide explains what each part does, why it matters, and then walks you through one of two deployment paths.

The same stack serves both ways to connect: the installable desktop app and a hosted browser client at app.<domain> — no install required, voice included.

SpacetimeDBAuth ServiceLiveKitMinIOBrowser ClientDiscovery URL

What Runs In Production (And Why)

SpacetimeDB

Real-time chat/game state backend. Required for channels, messages, membership, and sync.

Auth Service

Login/session API and token minting. Required for user authentication and LiveKit token creation.

LiveKit

Voice/media signaling and transport. Required for voice channels and real-time audio.

MinIO

S3-compatible object storage. Required for uploads, attachments, and download URLs. Large files use multipart PUTs (initially 64 MiB each), staying below Cloudflare's 100 MB per-request cap. The initial per-file limit is 500 MiB and the daily upload allowance is 2 GiB per user. Admins can also set per-user and installation stored-byte limits; both default to unlimited. A full MinIO volume is reported separately.

Module Init

One-shot container that publishes the SpacetimeDB WASM module on startup. Runs automatically and exits cleanly.

Web Client (app.<domain>)

Optional hosted browser SPA, pinned to this instance when its container starts, so users can run LetsChat in a browser — voice included — without installing the desktop app.

Discovery Endpoint (/.well-known/letschat.json)

Served by core-api on auth.<domain>, so app startup auto-discovers your backend from one URL (for example https://auth.example.com). No separate connect subdomain. Without this, users must paste a full join link manually.

Before applying setup or upgrade steps, reviewBreaking Changesto avoid version mismatch and migration issues.

Choose Your Deployment Path

Both paths deploy the same backend stack. The difference is how HTTPS traffic reaches your server.

Common Mistakes

ProblemLikely causeFix
Voice joins but no audioLiveKit media ports not forwardedForward 44382/udp (primary) and 44381/tcp (fallback) to host
Setup discovery failsauth.<domain> not serving /.well-known/letschat.jsonTunnel: auth → core-api:8787; Caddy: check AUTH_DOMAIN + DNS
Browser or desktop can't upload / download filesMINIO_CORS_ALLOW_ORIGIN narrowed to one originSet it back to * — the desktop app's origin is tauri://localhost, not the web one
Upload/download errorsWrong MINIO_PUBLIC_ENDPOINTSet to public https://files... endpoint clients can reach
Upload part fails with 413 (tunnel track)Cloudflare caps each proxied request body at 100 MB (Free/Pro)Keep the multipart part size at or below 90 MiB in the admin panel. Older clients still send one PUT and remain subject to Cloudflare's request cap.
Caddy cert issuance failsCloudflare proxy enabled or closed 80/443Use DNS-only records and open 80/tcp, 443/tcp
LiveKit setup fails immediatelyWrong signalling scheme (ws:// vs wss://)Both tracks use wss://lk... — signalling is TLS-terminated (Cloudflare tunnel ingress or Caddy). Plain ws:// is dev-only
Spacetime connect timeoutmodule-init failed or SpacetimeDB not yet healthyCheck docker logs letschat-module-init; it retries on failure automatically