SpacetimeDB
Real-time chat/game state backend. Required for channels, messages, membership, and sync.
Beginner-first production tutorial (Ubuntu + Docker Compose)
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.
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.
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. Builds the React bundle pinned to this instance, 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, review Breaking Changes to avoid version mismatch and migration issues.
Both paths deploy the same backend stack. The difference is how HTTPS traffic reaches your server.
Path A: Cloudflare Tunnel
What: Routes auth/chat/files/app and LiveKit signalling over Cloudflare Tunnel; only LiveKit media stays direct via forwarded ports.
Why: Keeps web services off direct public IP while still supporting voice traffic.
Best for: Home servers where you want Cloudflare ingress control without exposing 80/443 directly.
Read the guide →
Path B: Caddy Reverse Proxy
What: Caddy terminates TLS on your host and proxies auth/chat/files/lk/app locally.
Why: Simple standard architecture with fewer external moving parts.
Best for: Operators comfortable exposing 80/443 and managing direct DNS to host IP.
Read the guide →
| Problem | Likely cause | Fix |
|---|---|---|
| Voice joins but no audio | LiveKit media ports not forwarded | Forward 44382/udp (primary) and 44381/tcp (fallback) to host |
| Setup discovery fails | auth.<domain> not serving /.well-known/letschat.json | Tunnel: auth → core-api:8787; Caddy: set AUTH_DOMAIN + DNS |
| Browser client can't download files | MINIO_CORS_ALLOW_ORIGIN doesn't match the web origin | Set it to https://app.<domain> (or *) |
| Upload/download errors | Wrong MINIO_PUBLIC_ENDPOINT | Set to public https://files... endpoint clients can reach |
| Caddy cert issuance fails | Cloudflare proxy enabled or closed 80/443 | Use DNS-only records and open 80/tcp, 443/tcp |
| LiveKit setup fails immediately | Wrong 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 timeout | module-init failed or SpacetimeDB not yet healthy | Check docker logs letschat-module-init; it retries on failure automatically |