Concepts

Architecture

One application image plays three roles — API, worker and migrator — beside PostgreSQL, Valkey, object storage and a TURN server. Seven containers, about 170 MiB of RAM at rest.

Components #

ComponentRoleIf it dies
APIHTTP + both WebSocket endpoints; the SFU media loops run in-process.Clients reconnect; nothing is lost (state is in Postgres).
WorkerDurable jobs: attachment verification, thumbnails, push fan-out, webhook delivery, retention and erasure sweeps.Jobs resume — they are rows, claimed with FOR UPDATE SKIP LOCKED.
PostgreSQLSource of truth: rooms, members, messages, sequences, the outbox, jobs, keys, usage.The deployment is down. This is the one stateful dependency that matters.
ValkeyPresence, rate limits, fan-out pub/sub, ephemeral captures. Regenerable only.Degrades gracefully — it can be flushed without data loss.
Object storageAttachment blobs and thumbnails (S3-compatible, RustFS by default).Media unavailable; chat unaffected.
coturnSTUN/TURN for NAT traversal, with per-session time-limited credentials.Calls fail for users on restrictive networks.

How an event reaches every device #

This is the single most important path to understand, because every guarantee the platform makes follows from it.

  1. Write, then sequence — in one transaction The message row and an outbox row are inserted together and the transaction commits. The message gets a monotonically increasing per-room event_seq at that moment.
  2. Publish strictly after commit A single leased publisher claims outbox rows in order and pushes them to Valkey pub/sub. Because this happens after the commit, a cache or broker failure can never lose a message that a client was told was sent.
  3. Fan out to live sockets Every chat socket subscribed to that room receives the server envelope verbatim, carrying the event_id the client uses as its resume point.
  4. Fan out to everyone else The same published row drives durable jobs: a push to members with no live socket, and a signed webhook delivery to each subscribed endpoint. Retries and backoff are job-table mechanics.
The consequence: "delivered to the socket" and "durable" are different things, and durability wins. A client that was offline for a week catches up from its last sequence and misses nothing — see catch-up.

Media takes a different path #

Two things deliberately bypass the flow above:

  • Call media. RTP flows over UDP directly between each client and the SFU. Only signalling (SDP, ICE candidates) rides the WebSocket. The SFU forwards per subscriber, so a participant uploads one stream no matter how many people are in the room.
  • Attachment bytes. Clients upload and download through presigned URLs straight to object storage; the bytes never pass through the API. Upload throughput is therefore independent of API load.

Multi-tenancy #

Every record and every query is tenant-scoped, and cross-tenant access is refused at the authorisation layer rather than filtered afterwards. A customer API key is pinned to its tenant — passing a different tenant_id in a body does not change what it can reach.

Keys additionally carry an environment. A test key operates in a separate data namespace from a live key for the same tenant, so a sandbox integration can never read or write production rooms, and every token it mints inherits that isolation.

Deployment shape #

One compose file, one image, forward-only migrations applied automatically on start. Releases are built and gated on a test machine, then the built image is shipped to production — production never compiles. A 13-check smoke suite runs against production on every deploy and the release is refused if it fails.

Single-node by design. One SFU, one VM, no HA. Host loss is an outage until the machine is rebuilt. See Operations & limits for the measured capacity envelope and recovery numbers.