Skip to main content
Meet is the call surface, requested with urn:oximail:params:jmap:meet. The division of labour is the thing to understand first: OxiMail is the authority on who may join and who is currently in a call, and it relays the WebRTC handshake between participants. The media itself never touches the server.

The capability object

Read the capability rather than hardcoding any of it — this is where the shipped limits are announced:
stunServers: [] and turnServers: null are honest rather than empty by omission: the server ships no ICE servers of its own. Supply your own, and expect peer-to-peer to fail between two symmetric NATs without a TURN relay.

Objects and methods

A MeetSession carries id, accountId, chatRoomId, an optional calendarEventId, mode, config, participants, created, and the optional started and ended. participants is server-set — it is loaded from the store and never taken from a client. started is stamped the moment the second participant joins, which is when a session becomes a call rather than an invitation. A participant is flat on the wire — accountId, deviceId, displayName — with a role of moderator or participant and a status that phase 1 only ever sets to active or left.

A session is anchored on a room

chatRoomId is mandatory, and it is what authorization keys on: you may join a session if you are a member of its room. There is no separate invite list to keep in sync with the room. Creation fans a StateChanged for the MeetSession collection out to every member of the room, which is how a callee learns that a call exists without a dedicated notification type. That frame carries a state, not an id — so the second half of the loop is MeetSession/get with ids: null (RFC 8620 §5.1: every record the caller can see), which answers with the active sessions of the caller’s rooms. Those two together are the incoming-call discovery loop; a client needs nothing else to ring.

Membership is a lease, not a flag

Joining takes a lease keyed by (account, device). It expires 30 seconds after the last join or refresh, and MeetSession/refresh answers that window as expiresIn so a client can poll comfortably inside it rather than guessing. Sending a signal also counts as proof of life and touches the sender’s lease. When a lease expires, a sweeper flips that participant to left in the store and fans the change out; when nobody is active any more, the session ends by itself. This is why a browser tab that disappears does not leave a phantom in the call. Leases live in memory and are never persisted. A server restart therefore drops all of them, and liveness is re-derived from whatever fresh join/refresh calls re-establish — the same semantics chat presence has, where everyone starts offline after a restart.

Signaling is relayed, never stored

The four MeetSignal/* methods take sessionId, toAccountId, toDeviceId and fromDeviceId, plus the signal payload — sdp, candidate, reason — which is forwarded verbatim and never read.
  • Nothing is persisted, and nothing is logged. There is no signaling mailbox, no queue and no retry: the relay is transitory by contract. A refusal logs the session and the signal kind, never the content.
  • delivered is an ack about connections, not about people. true means at least one open WebSocket of the recipient received the frame. false means they have no open connection right now — not an error, and not queued for later. Handle it in your UX (“appears offline”), do not treat it as a failure to retry.
  • The server owns the frame’s identity fields. type, signal and fromAccountId are stamped after the caller’s payload is spread into the frame, so a caller cannot overwrite them by putting those keys in their own payload.
  • Both ends must be active participants. The sender is checked by (accountId, fromDeviceId) and the recipient by toAccountId/toDeviceId; anything else is forbidden, and any failure to resolve participants relays to nobody rather than to everybody.
  • Rate limited per (tenant, account, session) at 120 signals per minute by default — sized for trickle ICE, roughly two per second. Over budget answers the RFC 8620 §3.6.1 limit error.

What phase 1 does not do

The restrictions below are deliberate scope, not defects, and each one is observable from the capability object or from a named error rather than by trial:
  • Peer-to-peer only. mode is fixed to "p2p", sfuAvailable is false, and a join past three active participants answers tooManyEntries. There is no group mode to fall back to.
  • No recording, no waiting room. recordingAllowed and waitingRoom exist in MeetConfig’s shape but are refused with invalidProperties.
  • No guests. A participant’s identity is member or guest at the schema level, and no phase-1 path creates a guest.
  • No MeetParticipant/* methods — no admit, reject, kick or mute. Participants are read through the session.
  • One moderator, and it is the creator. Moderation is not transferable.
That last point has a consequence worth stating: because a session cannot outlive its moderator in any administrable form, erasing the creator’s account deletes the session as a whole rather than leaving an unadministrable one behind. Meet rows are part of what an account erasure removes. The chat surface these sessions anchor on is documented on the chat page; the session and push mechanics they rely on are on JMAP Core.