CLEMA protocol

Sessions

A persistent authentication session is an independently revocable device login with one rotating access/refresh token pair. A connection is a temporary transport channel. Several connections may authenticate with the same session, and one user may own several sessions. This distinction is independent of the selected transport and storage adapters.

For a message mutation, the initiating connection gets the updated state in its response while the user's other connections receive the durable event at the same place in its stream. A connection may be authenticated once; auth.phone or auth.token on an authenticated connection returns not_authorized.

List Sessions

{"id":"sessions-1","type":"session.list"}
{"id":"sessions-1","type":"session.list","ok":true,"data":{"sessions":[{"session_id":"s_0123...","transport":"websocket","client":{"device_name":"Alice's laptop","platform":"macOS","name":"Mandarin Web","version":"0.1.0"},"created_at":"2026-08-05T10:00:00Z","last_active_at":"2026-08-05T11:00:00Z","expires_at":"2026-09-04T11:00:00Z","current":true,"online":true,"push":false}]}}

Only the authenticated user's unexpired sessions are returned. Exactly one is current; online means at least one live connection uses that session, and push that its device is registered for push notifications.

transport names the adapter the login was made through, so an account's CLEMA devices and its clients on other protocols appear in one list and are revoked the same way. Unlike client, it is derived by the server and cannot be influenced by the client. It is empty for logins created before the field existed.

An account keeps at most 100 persistent sessions. Creating session 101 atomically removes the least recently active session, deauthenticates its live connections, and then creates the new one; Mandarin tells the account of a CLEMA client's session removed so. This hard bound keeps session.list small enough to return without pagination.

Revoke Sessions

Revoke one session by its opaque ID:

{"id":"sessions-2","type":"session.revoke","data":{"session_id":"s_0123..."}}

Or revoke every session except the current one:

{"id":"sessions-3","type":"session.revoke","data":{"scope":"others"}}

Exactly one selector is required. The response contains the affected IDs:

{"id":"sessions-3","type":"session.revoke","ok":true,"data":{"session_ids":["s_abcd..."]}}

Every live connection of an affected session is immediately deauthenticated and receives {"type":"session.revoked","data":{"session_id":"s_abcd...","reason":"revoked"}}, except the requesting connection when its successful response already conveys the result. Revoking another user's ID returns session_not_found and reveals nothing about its existence.

reason is revoked for an explicit revoke or logout, expired when expires_at is reached, and replaced when the per-account session limit evicts the least recently active login.

Presence follows reachability. A user is online while they can be reached: at least one of their connections is open, active or not, or at least one of their sessions is registered for pushes. Contacts receive presence.online when the first such way appears and presence.offline when the last one goes. Extra connections or sessions cause no transition. A user who made themselves invisible is offline as far as their contacts can tell, however they can be reached.

Slow clients are dropped. Outgoing messages are queued per connection. A client that stops reading long enough to fill that queue is disconnected rather than allowed to stall senders.