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 fourMeetSignal/* 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.
deliveredis an ack about connections, not about people.truemeans at least one open WebSocket of the recipient received the frame.falsemeans 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,signalandfromAccountIdare 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 bytoAccountId/toDeviceId; anything else isforbidden, 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.
modeis fixed to"p2p",sfuAvailableisfalse, and a join past three active participants answerstooManyEntries. There is no group mode to fall back to. - No recording, no waiting room.
recordingAllowedandwaitingRoomexist inMeetConfig’s shape but are refused withinvalidProperties. - 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.