This document describes the Meeting Backend API endpoints for creating meetings, managing the waiting room, and issuing room access tokens.
See Also: Meeting Ownership & Architecture for the system architecture, two-service model, token flow, and meeting lifecycle.
The Meeting Backend is a separate microservice (its own binary, process, and port) that handles all meeting business logic. It issues signed JWT room access tokens that clients use to connect to the Media Server.
The meeting flow:
- Host creates a meeting (or one is auto-created on first join)
- Host joins the meeting, activating it and receiving a room access token
- Attendees request to join and enter the waiting room
- Host admits or rejects attendees
- Admitted attendees receive a push notification via the media server connection, then fetch their room access token via
GET /status - Clients connect to the Media Server using their room access token
- The Media Server rejects any connection without a valid, signed token (when
FEATURE_MEETING_MANAGEMENT=true)
All request types, response types, error types, and JWT claims are defined in the videocall-meeting-types crate. This crate is framework-agnostic (no actix-web, no database types) and serves as the single source of truth for the API contract.
Key types:
| Type | Location | Description |
|---|---|---|
APIResponse<A> |
responses.rs |
Envelope: { "success": bool, "result": A } |
APIError |
error.rs |
Error payload with code, message, engineering_error |
CreateMeetingRequest |
requests.rs |
Request body for meeting creation |
JoinMeetingRequest |
requests.rs |
Request body for joining a meeting |
AdmitRequest |
requests.rs |
Request body for admit/reject |
ListMeetingsQuery |
requests.rs |
Query parameters for listing meetings |
ParticipantStatusResponse |
responses.rs |
Canonical participant shape (used across multiple endpoints) |
RoomAccessTokenClaims |
token.rs |
JWT claims for room access tokens |
Every endpoint returns an APIResponse<T> envelope:
{ "success": true, "result": { ... } }
{ "success": false, "result": { "code": "MEETING_NOT_FOUND", "message": "..." } }All success and error examples below show the full envelope.
All meeting-api endpoints (except OAuth login/callback and logout) require a valid signed session JWT.
Pass the session JWT in the Authorization header:
-H "Authorization: Bearer <session_jwt>"The session JWT is obtained after a successful OAuth login via GET /login. The OAuth callback issues the token, and all subsequent API calls must include it in the Authorization: Bearer header.
Browser note: The web UI (
dioxus-ui) uses anHttpOnlysession cookie that the browser sends automatically. This is an implementation detail of the browser client -- for API testing, CLI tools, mobile apps, and all documentation examples, always use theAuthorization: Bearerheader.
The session JWT contains these claims:
| Claim | Description |
|---|---|
sub |
User email (identity principal) |
name |
Display name |
exp |
Expiration (Unix timestamp) |
iat |
Issued-at (Unix timestamp) |
iss |
"videocall-meeting-backend" |
Note: The session JWT authenticates requests to the Meeting Backend only. To connect to the Media Server, clients must present a separate room access token (JWT) issued by the Meeting Backend when a participant is admitted to a meeting.
The Meeting Backend supports cross-origin requests with credentials. Set CORS_ALLOWED_ORIGIN to the exact frontend origin in production (e.g. https://app.videocall.rs). When unset, the server mirrors the request Origin for development convenience. See Meeting Ownership & Architecture for deployment recommendations.
The room access token is a signed JWT (HMAC-SHA256) that authorizes a client to connect to the Media Server for a specific room. See Meeting Ownership & Architecture for the full token specification.
Key points:
- Issued when a participant's status becomes
admitted - Scoped to a specific room and participant
- Contains identity, room, host status, and display name
- Single-burner design: tokens have a short TTL (default: 60 seconds, configurable via
TOKEN_TTL_SECS). They are intended as one-time admission tickets, not long-lived credentials - Delivered in the
room_tokenfield of API responses - A fresh token is generated on every call to
GET /api/v1/meetings/{id}/statuswhen the participant is admitted
The following diagram shows the complete lifecycle of a room access token, including the automatic refresh flow when a media server connection is lost:
sequenceDiagram
participant UI as dioxus-ui
participant API as meeting-api :8081
participant MS as media-server :8080
rect rgb(40, 40, 60)
note right of UI: Initial Connection
UI->>API: POST /api/v1/meetings/{id}/join
API-->>UI: 200 OK + room_token (60s TTL)
UI->>MS: WebSocket /lobby?token=<JWT>
MS->>MS: Validate JWT signature + expiry
MS-->>UI: 101 Switching Protocols
note over UI, MS: Video call in progress...
end
rect rgb(60, 40, 40)
note right of UI: Connection Lost (network drop, server restart, etc.)
MS--xUI: Connection closed
end
rect rgb(40, 60, 40)
note right of UI: Auto-Refresh and Reconnect
UI->>API: GET /api/v1/meetings/{id}/status
API-->>UI: 200 OK + new room_token (60s TTL)
UI->>MS: WebSocket /lobby?token=<newJWT>
MS->>MS: Validate JWT signature + expiry
MS-->>UI: 101 Switching Protocols
note over UI, MS: Video call resumed
end
Why single-burner tokens?
- Security: Even if a token is intercepted, it expires in 60 seconds and cannot be reused for long
- Revocation: No need for a token revocation list; expired tokens are automatically invalid
- Simplicity: The media server only needs to validate the JWT signature and expiry, with no database lookup required
Error handling on the media server:
| Token Error | HTTP Response | Description |
|---|---|---|
| Expired | 401 Unauthorized |
Token was valid but has expired. Client should fetch a fresh token. |
| Invalid signature | 403 Forbidden |
Token has been tampered with. This incident is logged. |
| Missing | 401 Unauthorized |
No token provided. Use /lobby?token=<JWT>. |
| Room join denied | 403 Forbidden |
Token does not grant room join permission. |
| State | Description |
|---|---|
idle |
Meeting created but host hasn't joined yet |
active |
Host has joined, room access token issued, meeting is in progress |
ended |
Meeting has ended (host left or all participants left) |
Note: A meeting automatically transitions to
endedwhen:
- The host leaves the meeting, OR
- All admitted participants have left the meeting
| Status | Description |
|---|---|
waiting_for_meeting |
Meeting exists but is not yet active (host hasn't joined). Observer token provided for push notifications. |
waiting |
In waiting room, pending approval. No room token issued. Observer token provided for push notifications. |
admitted |
Approved by host. Room access token available. |
rejected |
Denied entry by host. |
left |
Previously in meeting, now left. |
All timestamps in API responses are Unix seconds (not milliseconds). This applies to created_at, started_at, ended_at, joined_at, and admitted_at.
All endpoints are served by the meeting-api on port 8081 (both local development and production).
The UI's apiBaseUrl should point to http://localhost:8081 for local development (this is the default in docker-compose.yaml). The media server (WebSocket/WebTransport) runs separately on port 8080.
Lists all meetings owned by the authenticated user (excludes deleted meetings, includes ended meetings).
GET /api/v1/meetings
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer | 20 | Maximum number of meetings to return (1-100) |
offset |
integer | 0 | Number of meetings to skip for pagination |
Response (200 OK):
{
"success": true,
"result": {
"meetings": [
{
"meeting_id": "standup-2024",
"host": "host@example.com",
"state": "active",
"has_password": false,
"created_at": 1706918400,
"participant_count": 3,
"started_at": 1706918400,
"ended_at": null,
"waiting_count": 1
}
],
"total": 1,
"limit": 20,
"offset": 0
}
}Rust type:
APIResponse<ListMeetingsResponse>(each entry is aMeetingSummary)
Note: Ended meetings remain in the list until the owner explicitly deletes them. This allows owners to rejoin or restart meetings with the same ID.
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED |
Invalid or missing session |
Error example (401):
{
"success": false,
"result": {
"code": "UNAUTHORIZED",
"message": "Authentication required."
}
}Creates a new meeting. The authenticated user becomes the host. The meeting starts in idle state.
POST /api/v1/meetings
Request Body:
{
"meeting_id": "my-meeting",
"attendees": ["user@example.com"],
"password": "secret123"
}| Field | Type | Required | Description |
|---|---|---|---|
meeting_id |
string | No | Meeting identifier. Auto-generated (12 chars) if omitted. |
attendees |
string[] | No | Pre-registered attendee emails (max 100). |
password |
string | No | Meeting password (hashed with Argon2 before storage). |
Response (201 Created):
{
"success": true,
"result": {
"meeting_id": "my-meeting",
"host": "host@example.com",
"created_at": 1706918400,
"state": "idle",
"attendees": ["user@example.com"],
"has_password": true
}
}Rust type:
APIResponse<CreateMeetingResponse>
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED |
Invalid or missing session |
| 400 | INVALID_MEETING_ID |
Invalid ID format |
| 400 | TOO_MANY_ATTENDEES |
More than 100 attendees |
| 409 | MEETING_EXISTS |
Meeting ID already taken |
Error example (409):
{
"success": false,
"result": {
"code": "MEETING_EXISTS",
"message": "Meeting with ID 'my-meeting' already exists"
}
}Retrieves meeting information and your participation status.
GET /api/v1/meetings/{meeting_id}
Response (200 OK):
{
"success": true,
"result": {
"meeting_id": "my-meeting",
"state": "active",
"host": "host@example.com",
"host_display_name": "Alice",
"has_password": false,
"your_status": {
"email": "attendee@example.com",
"display_name": "Bob",
"status": "waiting",
"is_host": false,
"joined_at": 1706918500,
"admitted_at": null,
"room_token": null
}
}
}Rust type:
APIResponse<MeetingInfoResponse>(with nestedParticipantStatusResponse)
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED |
Invalid or missing session |
| 404 | MEETING_NOT_FOUND |
Meeting does not exist |
Soft-deletes a meeting. Only the meeting owner can delete their meetings.
- Sets
deleted_attimestamp (soft delete) - Meeting no longer appears in "My Meetings" list
- The meeting ID can be reused by any user after deletion
DELETE /api/v1/meetings/{meeting_id}
Response (200 OK):
{
"success": true,
"result": {
"message": "Meeting 'my-meeting' has been deleted"
}
}Rust type:
APIResponse<DeleteMeetingResponse>
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED |
Invalid or missing session |
| 403 | NOT_OWNER |
Not the meeting owner |
| 404 | MEETING_NOT_FOUND |
Meeting does not exist |
Request to join a meeting. If the meeting doesn't exist, it will be automatically created with the joining user as the owner/host.
- First user to join becomes the host; the meeting is created and activated
- Hosts are auto-admitted and receive a
room_tokenimmediately - Attendees (non-hosts) enter the waiting room (no
room_tokenuntil admitted)
POST /api/v1/meetings/{meeting_id}/join
Request Body (optional):
{
"display_name": "Alice"
}Response for hosts (200 OK):
{
"success": true,
"result": {
"email": "host@example.com",
"display_name": "Alice",
"status": "admitted",
"is_host": true,
"joined_at": 1706918400,
"admitted_at": 1706918400,
"room_token": "eyJhbGciOiJIUzI1NiIs..."
}
}Response for attendees in waiting room (200 OK):
{
"success": true,
"result": {
"email": "attendee@example.com",
"display_name": "Bob",
"status": "waiting",
"is_host": false,
"joined_at": 1706918500,
"admitted_at": null,
"room_token": null,
"observer_token": "eyJhbGciOiJIUzI1NiIs..."
}
}Response when meeting is not yet active (200 OK):
{
"success": true,
"result": {
"email": "attendee@example.com",
"display_name": "Bob",
"status": "waiting_for_meeting",
"is_host": false,
"joined_at": 1706918500,
"admitted_at": null,
"room_token": null,
"observer_token": "eyJhbGciOiJIUzI1NiIs...",
"waiting_room_enabled": true,
"host_display_name": null
}
}Rust type:
APIResponse<ParticipantStatusResponse>
The room_token is only present when status is "admitted". Attendees receive push notifications via their media server connection (using the observer_token) when their status changes. The observer_token allows waiting participants to connect to the media server in observer mode to receive these notifications.
Note: When the meeting exists but the host hasn't joined yet, a
waiting_for_meetingstatus is returned instead of an error. The client can use theobserver_tokento listen for aMEETING_ACTIVATEDpush notification.
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED |
Invalid or missing session |
Note: If the meeting doesn't exist, it is created automatically with the joining user as the host.
Lists all participants waiting to be admitted. Any admitted participant can view the waiting room.
GET /api/v1/meetings/{meeting_id}/waiting
Response (200 OK):
{
"success": true,
"result": {
"meeting_id": "my-meeting",
"waiting": [
{
"email": "attendee1@example.com",
"display_name": "Bob",
"status": "waiting",
"is_host": false,
"joined_at": 1706918500,
"admitted_at": null,
"room_token": null
},
{
"email": "attendee2@example.com",
"display_name": "Charlie",
"status": "waiting",
"is_host": false,
"joined_at": 1706918510,
"admitted_at": null,
"room_token": null
}
]
}
}Rust type:
APIResponse<WaitingRoomResponse>(each entry is aParticipantStatusResponse)
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED |
Invalid or missing session |
| 403 | NOT_HOST |
Requester is not an admitted participant |
| 404 | MEETING_NOT_FOUND |
Meeting does not exist |
Admits a participant from the waiting room. Any admitted participant can admit others. A room access token is generated for the admitted participant.
POST /api/v1/meetings/{meeting_id}/admit
Request Body:
{
"email": "attendee@example.com"
}Response (200 OK):
{
"success": true,
"result": {
"email": "attendee@example.com",
"display_name": "Bob",
"status": "admitted",
"is_host": false,
"joined_at": 1706918500,
"admitted_at": 1706918600,
"room_token": null
}
}Rust type:
APIResponse<ParticipantStatusResponse>
The admitted participant receives a PARTICIPANT_ADMITTED push notification via their media server connection and then fetches their room_token via GET /status. The room_token is null in the admit response because the token is delivered to the participant, not to the admitter.
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED |
Invalid or missing session |
| 403 | NOT_HOST |
Requester is not an admitted participant |
| 404 | PARTICIPANT_NOT_FOUND |
Participant not in waiting room |
| 404 | MEETING_NOT_FOUND |
Meeting does not exist |
Admits all participants currently in the waiting room at once. Room access tokens are generated for each admitted participant.
POST /api/v1/meetings/{meeting_id}/admit-all
Response (200 OK):
{
"success": true,
"result": {
"admitted_count": 2,
"admitted": [
{
"email": "attendee1@example.com",
"display_name": "Bob",
"status": "admitted",
"is_host": false,
"joined_at": 1706918500,
"admitted_at": 1706918600,
"room_token": null
},
{
"email": "attendee2@example.com",
"display_name": "Charlie",
"status": "admitted",
"is_host": false,
"joined_at": 1706918510,
"admitted_at": 1706918600,
"room_token": null
}
]
}
}Rust type:
APIResponse<AdmitAllResponse>(each entry is aParticipantStatusResponse)
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED |
Invalid or missing session |
| 403 | NOT_HOST |
Requester is not an admitted participant |
| 404 | MEETING_NOT_FOUND |
Meeting does not exist |
Rejects a participant from the waiting room.
POST /api/v1/meetings/{meeting_id}/reject
Request Body:
{
"email": "attendee@example.com"
}Response (200 OK):
{
"success": true,
"result": {
"email": "attendee@example.com",
"display_name": "Bob",
"status": "rejected",
"is_host": false,
"joined_at": 1706918500,
"admitted_at": null,
"room_token": null
}
}Rust type:
APIResponse<ParticipantStatusResponse>
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED |
Invalid or missing session |
| 403 | NOT_HOST |
Requester is not an admitted participant |
| 404 | PARTICIPANT_NOT_FOUND |
Participant not in waiting room |
Check your current status in a meeting. While clients can poll this endpoint, the primary notification mechanism is push via NATS events through the media server connection. When status becomes admitted, the response includes the room_token needed to connect to the Media Server.
GET /api/v1/meetings/{meeting_id}/status
Response when waiting (200 OK):
{
"success": true,
"result": {
"email": "attendee@example.com",
"display_name": "Bob",
"status": "waiting",
"is_host": false,
"joined_at": 1706918500,
"admitted_at": null,
"room_token": null
}
}Response when admitted (200 OK):
{
"success": true,
"result": {
"email": "attendee@example.com",
"display_name": "Bob",
"status": "admitted",
"is_host": false,
"joined_at": 1706918500,
"admitted_at": 1706918600,
"room_token": "eyJhbGciOiJIUzI1NiIs..."
}
}Rust type:
APIResponse<ParticipantStatusResponse>
The client should use the room_token to connect to the Media Server immediately upon receiving it.
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED |
Invalid or missing session |
| 404 | NOT_IN_MEETING |
Haven't requested to join |
Leave a meeting. The meeting automatically ends when:
- The host leaves, OR
- All admitted participants have left
POST /api/v1/meetings/{meeting_id}/leave
Response (200 OK):
{
"success": true,
"result": {
"email": "attendee@example.com",
"display_name": "Bob",
"status": "left",
"is_host": false,
"joined_at": 1706918500,
"admitted_at": 1706918600,
"room_token": null
}
}Rust type:
APIResponse<ParticipantStatusResponse>
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED |
Invalid or missing session |
| 404 | NOT_IN_MEETING |
Not a participant in this meeting |
Lists all admitted participants currently in the meeting.
GET /api/v1/meetings/{meeting_id}/participants
Response (200 OK):
{
"success": true,
"result": [
{
"email": "host@example.com",
"display_name": "Alice",
"status": "admitted",
"is_host": true,
"joined_at": 1706918400,
"admitted_at": 1706918400,
"room_token": null
},
{
"email": "attendee@example.com",
"display_name": "Bob",
"status": "admitted",
"is_host": false,
"joined_at": 1706918500,
"admitted_at": 1706918600,
"room_token": null
}
]
}Rust type:
APIResponse<Vec<ParticipantStatusResponse>>Note: The
room_tokenfield isnullin participant listings. Tokens are only delivered to the participant themselves viaPOST /joinorGET /status.
Errors:
| Status | Code | Description |
|---|---|---|
| 401 | UNAUTHORIZED |
Invalid or missing session |
| 404 | MEETING_NOT_FOUND |
Meeting does not exist |
After receiving a room_token, the client connects to the Media Server using the token-based endpoint:
GET /lobby?token=<room_access_token>
- WebSocket:
ws://host:8080/lobby?token=<JWT> - WebTransport:
https://host:4433/lobby?token=<JWT>
The identity (email) and room are extracted from the JWT claims (sub and room). There are no email or room parameters in the URL -- the token is the sole source of truth.
The Media Server:
- Validates the JWT signature using the shared
JWT_SECRET - Checks the
expclaim (rejects expired tokens) - Verifies
room_join == true - Extracts
sub(identity),room(room ID),is_host, anddisplay_name - Establishes the WebSocket or WebTransport connection
- Rejects the connection if the token is missing, invalid, or expired
The legacy path-based endpoint is still available for backward compatibility:
GET /lobby/{email}/{room}
This endpoint is deprecated and only works when FEATURE_MEETING_MANAGEMENT=false. When meeting management is enabled, it returns HTTP 410 Gone. Clients should migrate to the token-based endpoint above.
The simplest way to start a meeting -- just join any meeting ID. If it doesn't exist, you become the host and receive a token immediately.
sequenceDiagram
participant U as FirstUser
participant MB as MeetingBackend
participant MS as MediaServer
U->>MB: POST /api/v1/meetings/my-room/join
Note over MB: Meeting not found, create with user as host
MB-->>U: success: true, result.status: admitted, result.room_token: "ey..."
U->>MS: GET /lobby?token=ey...
Note over MS: Decode JWT, extract room + identity from claims
MS-->>U: WebSocket connection established
For scheduled meetings, explicitly create the meeting first.
sequenceDiagram
participant H as Host
participant MB as MeetingBackend
participant MS as MediaServer
participant A as Attendee
H->>MB: POST /api/v1/meetings
MB-->>H: success: true, result.state: idle
H->>MB: POST /api/v1/meetings/{id}/join
MB-->>H: success: true, result.status: admitted, result.room_token: "ey..."
H->>MS: GET /lobby?token=ey...
MS-->>H: Connection established
A->>MB: POST /api/v1/meetings/{id}/join
MB-->>A: success: true, result.status: waiting, result.observer_token: "ey..."
A->>MS: GET /lobby?token=observer_token (observer mode)
Note over A,MS: Waiting for push notification...
H->>MB: POST /api/v1/meetings/{id}/admit
Note over MB: Generate room token, publish NATS event
MB->>MS: NATS: PARTICIPANT_ADMITTED
MS-->>A: Push notification: admitted
A->>MB: GET /api/v1/meetings/{id}/status
MB-->>A: success: true, result.status: admitted, result.room_token: "ey..."
A->>MS: GET /lobby?token=room_token
MS-->>A: Connection established
A rejected attendee never receives a token and cannot connect to the Media Server.
sequenceDiagram
participant A as Attendee
participant MB as MeetingBackend
participant MS as MediaServer
A->>MB: POST /api/v1/meetings/{id}/join
MB-->>A: success: true, result.status: waiting
Note over MB: Host rejects attendee
A->>MB: GET /api/v1/meetings/{id}/status
MB-->>A: success: true, result.status: rejected, result.room_token: null
Note over A,MS: No token = no connection possible
# Meeting API runs on port 8081 (both local dev and production).
# Media Server (WebSocket/WebTransport) runs on port 8080.
#
# Replace $HOST_TOKEN and $ATTENDEE_TOKEN with session JWTs obtained after OAuth login.
# 1. Host creates meeting
curl -X POST http://localhost:8081/api/v1/meetings \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $HOST_TOKEN" \
-d '{"meeting_id": "standup-2024"}'
# Response:
# {"success":true,"result":{"meeting_id":"standup-2024","host":"host@example.com",
# "created_at":1706918400,"state":"idle","attendees":[],"has_password":false}}
# 2. Host joins meeting (activates it, receives room token)
curl -X POST http://localhost:8081/api/v1/meetings/standup-2024/join \
-H "Authorization: Bearer $HOST_TOKEN"
# Response:
# {"success":true,"result":{"email":"host@example.com","display_name":null,
# "status":"admitted","is_host":true,"joined_at":1706918400,
# "admitted_at":1706918400,"room_token":"eyJhbGciOiJIUzI1NiIs..."}}
# 3. Host connects to Media Server with the room token
# (In practice, the client UI does this automatically)
# WebSocket: ws://localhost:8080/lobby?token=eyJhbGciOiJIUzI1NiIs...
# 4. Attendee tries to join
curl -X POST http://localhost:8081/api/v1/meetings/standup-2024/join \
-H "Authorization: Bearer $ATTENDEE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"display_name": "Alice"}'
# Response:
# {"success":true,"result":{"email":"alice@example.com","display_name":"Alice",
# "status":"waiting","is_host":false,"joined_at":1706918500,
# "admitted_at":null,"room_token":null}}
# 5. Host checks waiting room
curl http://localhost:8081/api/v1/meetings/standup-2024/waiting \
-H "Authorization: Bearer $HOST_TOKEN"
# Response:
# {"success":true,"result":{"meeting_id":"standup-2024",
# "waiting":[{"email":"alice@example.com","display_name":"Alice",...}]}}
# 6. Host admits Alice
curl -X POST http://localhost:8081/api/v1/meetings/standup-2024/admit \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $HOST_TOKEN" \
-d '{"email": "alice@example.com"}'
# 7. Alice receives a push notification and fetches her room token
curl http://localhost:8081/api/v1/meetings/standup-2024/status \
-H "Authorization: Bearer $ATTENDEE_TOKEN"
# Response:
# {"success":true,"result":{"email":"alice@example.com","display_name":"Alice",
# "status":"admitted","is_host":false,"joined_at":1706918500,
# "admitted_at":1706918600,"room_token":"eyJhbGciOiJIUzI1NiIs..."}}
# 8. Alice connects to Media Server with her room token
# WebSocket: ws://localhost:8080/lobby?token=eyJhbGciOiJIUzI1NiIs...
# 9. When done, participants leave
curl -X POST http://localhost:8081/api/v1/meetings/standup-2024/leave \
-H "Authorization: Bearer $ATTENDEE_TOKEN"
# 10. Host leaves (ends the meeting)
curl -X POST http://localhost:8081/api/v1/meetings/standup-2024/leave \
-H "Authorization: Bearer $HOST_TOKEN"All tables are owned by the Meeting Backend. The Media Server does not access them.
| Column | Type | Description |
|---|---|---|
| id | SERIAL | Primary key |
| room_id | VARCHAR(255) | Unique meeting identifier |
| creator_id | VARCHAR(255) | Host email |
| state | VARCHAR(50) | idle, active, ended |
| password_hash | VARCHAR(255) | Argon2 hashed password |
| attendees | JSONB | Pre-registered attendees |
| started_at | TIMESTAMPTZ | When meeting started |
| ended_at | TIMESTAMPTZ | When meeting ended |
| deleted_at | TIMESTAMPTZ | Soft delete timestamp |
| host_display_name | VARCHAR(255) | Cached host display name |
Single source of truth for participant state. Replaces the legacy session_participants table.
| Column | Type | Description |
|---|---|---|
| id | SERIAL | Primary key |
| meeting_id | INTEGER | Foreign key to meetings |
| VARCHAR(255) | Participant email | |
| display_name | VARCHAR(255) | Participant's chosen display name |
| status | VARCHAR(50) | waiting, admitted, rejected, left |
| is_host | BOOLEAN | Whether this is the host |
| joined_at | TIMESTAMPTZ | When joined/entered waiting room |
| admitted_at | TIMESTAMPTZ | When admitted by host |
| left_at | TIMESTAMPTZ | When left the meeting |
Both the Meeting Backend and the Media Server have comprehensive test suites. All tests run via a single entry point.
# Run all backend tests (requires Docker for integration tests)
make tests_run
# Tear down test containers
make tests_downmake tests_run performs the following in order:
cargo fmt --check(workspace-wide)cargo clippy -- -D warnings(workspace-wide)cargo machete(unused dependency check)- Docker integration tests (both
meeting-apiandvideocall-apiagainst PostgreSQL)