Note
This repo is only one part of the bigger peer metrics WebRTC monitoring service. Check out the full project here.
This folder contains code for the API server used to ingest the metrics sent by the SDK.
This is a public API endpoint that has two functions:
- ingesting data collected by the SDK
- used for data query by
web
In addition to this, the api has the django admin interface to check the raw data collected.
Before running the API server locally, ensure you have the following installed:
- Python 3.8 - The application is built with Python 3.8
- Docker & Docker Compose - For containerized deployment (recommended)
- PostgreSQL - Database for storing metrics (if running without Docker)
- Redis - For caching (optional but recommended)
The application uses environment variables for configuration. Copy the .env file and configure the following key variables:
# Django settings
DEBUG=True
DJANGO_SETTINGS_MODULE=api.settings
SECRET_KEY=your-secret-key-here
# JWT token secrets - used for authentication
INIT_TOKEN_SECRET=your-init-token-secret
SESSION_TOKEN_SECRET=your-session-token-secret
# Web domain for CORS
WEB_DOMAIN=localhost:8080
# Redis configuration (optional)
REDIS_HOST=redis://127.0.0.1:6379
# Database configuration
DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_USER=peeruser
DATABASE_PASSWORD=peeruser
DATABASE_NAME=peerdb
CONN_MAX_AGE=14400
# Optional: Post-conference cleanup
POST_CONFERENCE_CLEANUP=FalseKey Environment Variables Explained:
INIT_TOKEN_SECRET: Secret used to sign JWT tokens returned by the/initializeendpointSESSION_TOKEN_SECRET: Secret used to sign JWT tokens returned by the/sessionsendpointWEB_DOMAIN: Domain for CORS configuration, allows the web interface to query the APIDATABASE_*: PostgreSQL connection parametersREDIS_HOST: Redis server location for caching (improves performance)POST_CONFERENCE_CLEANUP: IfTrue, deletes unnecessary stats events after a conference ends
The application uses PostgreSQL as its database. The database schema is managed through Django migrations.
With Docker: The database is automatically created and migrations are applied when using Docker Compose.
Without Docker:
- Create a PostgreSQL database:
createdb peerdb- Run migrations:
python manage.py migrate- Create a superuser for Django admin access:
python manage.py createsuperuserThe recommended way to run the API server locally is using Docker:
- Build and start the containers:
docker-compose up --build-
The API server will be available at
http://localhost:8000 -
To run in detached mode:
docker-compose up -d- To stop the containers:
docker-compose downTo run the API server without Docker:
- Install dependencies:
pip install -r requirements.txt-
Ensure PostgreSQL is running and configured in your
.envfile -
Run migrations:
python manage.py migrate- Start the development server:
python manage.py runserver- The API server will be available at
http://localhost:8000
For production deployments, use a WSGI server like Gunicorn:
gunicorn api.wsgi:application --bind 0.0.0.0:8000The Django admin interface allows you to view and manage the raw data collected by the API.
- Create a superuser account (if not already done):
python manage.py createsuperuser-
Access the admin interface at
http://localhost:8000/admin -
Log in with your superuser credentials
The admin interface provides access to all models including Organizations, Apps, Conferences, Sessions, Participants, Connections, and Events.
The API uses a two-tier JWT token authentication system to secure endpoints. The authentication flow differs between SDK (public) endpoints and web interface (private) endpoints.
Each App has a unique API key used for initial authentication. The API key:
- Is a 32-character alphanumeric string
- Must be included in the first request to
/initialize - Is validated against the App model in the database
- Can be regenerated through the web interface if compromised
Example API Key:
a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
The authentication process involves two types of JWT tokens:
1. Initialize Token (from /initialize endpoint)
When the SDK first connects, it calls /initialize with an API key:
POST /initialize
{
"apiKey": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"conferenceId": "room-123",
"userId": "user-456"
}Response includes an initialize token:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"getStatsInterval": 10000,
"batchConnectionEvents": false,
"time": 1234567890.123
}The initialize token payload contains:
{
"p": "participant-uuid",
"c": "conference-uuid",
"t": 1234567890.123
}Token lifespan: 24 hours (configurable via INIT_TOKEN_LIFESPAN)
2. Session Token (from /sessions endpoint)
The initialize token is then used to create a session:
POST /sessions
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"appVersion": "1.0.0",
"platform": {...},
"devices": {...}
}Response includes a session token:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}The session token payload contains:
{
"s": "session-uuid",
"t": 1234567890.123
}Token lifespan: 24 hours (configurable via SESSION_TOKEN_LIFESPAN)
3. Authenticated Requests
All subsequent requests use the session token:
POST /stats
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"connectionId": "connection-uuid",
"data": {...}
}Public Endpoints (used by SDK):
- Do not require user authentication
- Use JWT tokens for authorization
- Rate-limited to prevent abuse
- Accept requests from any origin (with optional domain restrictions per App)
Private Endpoints (used by web interface):
- Require user authentication (session-based)
- Used for querying and managing data
- CORS-enabled for configured web domains
- Support filtering and pagination
The standard flow for SDK integration follows these steps:
- Initialize: SDK calls
/initializewith API key → receives init token - Create Session: SDK calls
POST /sessionswith init token → receives session token - Send Events: SDK sends events (getUserMedia, connections, stats) with session token
- Query Data: Web interface queries data using private endpoints
Step 1: Initialize Connection
// SDK initialization
const response = await fetch('http://localhost:8000/initialize', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
apiKey: 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6',
conferenceId: 'room-123',
conferenceName: 'Team Meeting',
userId: 'user-456',
userName: 'John Doe'
})
});
const { token: initToken, getStatsInterval } = await response.json();Step 2: Create Session
const sessionResponse = await fetch('http://localhost:8000/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: initToken,
appVersion: '1.0.0',
webrtcSdk: 'native',
platform: {
name: 'Chrome',
version: '96.0.4664.110',
os: 'Windows'
},
devices: {
audio: { deviceId: 'default' },
video: { deviceId: 'camera-1' }
}
})
});
const { token: sessionToken } = await sessionResponse.json();Step 3: Send Stats
// Send WebRTC stats periodically
setInterval(async () => {
await fetch('http://localhost:8000/stats', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: sessionToken,
connectionId: 'connection-uuid',
data: {
connection: {
local: { candidateType: 'host' },
remote: { candidateType: 'srflx' }
},
audio: {
inbound: [...],
outbound: [...]
},
video: {
inbound: [...],
outbound: [...]
}
}
})
});
}, getStatsInterval);Step 4: Send Connection Events
// When a peer connection is created
await fetch('http://localhost:8000/connection', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: sessionToken,
type: 'addConnection',
peerId: 'peer-uuid',
peerName: 'Jane Doe',
connectionState: 'connecting'
})
});Query Sessions for a Conference
GET /sessions?conferenceId=conference-uuid
Response:
{
"data": [
{
"id": "session-uuid",
"participant": "participant-uuid",
"conference": "conference-uuid",
"created_at": "2024-01-15T10:30:00Z",
"end_time": "2024-01-15T11:00:00Z",
"duration": 1800,
"platform": {...},
"devices": {...}
}
]
}Query Conference Details
GET /conferences/conference-uuid
Response:
{
"data": {
"id": "conference-uuid",
"conference_id": "room-123",
"conference_name": "Team Meeting",
"start_time": "2024-01-15T10:30:00Z",
"end_time": "2024-01-15T11:00:00Z",
"duration": 1800,
"ongoing": false,
"app": "app-uuid"
}
}Query Conference Events
GET /conferences/conference-uuid/events?type=stats
Response:
{
"data": [
{
"id": "event-uuid",
"type": "stats",
"category": "S",
"data": {...},
"created_at": "2024-01-15T10:30:15Z",
"participant": "participant-uuid",
"session": "session-uuid"
}
]
}All errors follow a consistent JSON format:
{
"error_code": "missing_parameters",
"message": "Some parameters are missing from the request."
}The API uses standard HTTP status codes:
200 OK- Request succeeded400 Bad Request- Invalid parameters or malformed request401 Unauthorized- Missing, invalid, or expired token403 Forbidden- Domain not allowed or insufficient permissions404 Not Found- Resource does not exist405 Method Not Allowed- HTTP method not supported for endpoint500 Internal Server Error- Server-side error
Authentication Errors:
// Missing token
{
"error_code": "missing_token",
"message": "No token supplied."
}
// Invalid token
{
"error_code": "invalid_token",
"message": "Invalid token."
}
// Expired token
{
"error_code": "expired_token",
"message": "Expired token."
}
// Invalid API key
{
"error_code": "invalid_api_key",
"message": "Invalid api key."
}Validation Errors:
// Missing parameters
{
"error_code": "missing_parameters",
"message": "Some parameters are missing from the request."
}
// Invalid parameters
{
"error_code": "invalid_parameters",
"message": "Some supplied parameters are not valid."
}
// Domain not allowed
{
"error_code": "domain_not_allowed",
"message": "The app does not allow this domain."
}Resource Errors:
// Conference not found
{
"error_code": "conference_not_found",
"message": "The requested conference does not exist."
}
// Participant not found
{
"error_code": "participant_not_found",
"message": "The requested participant does not exist."
}
// Connection not found
{
"error_code": "connection_not_found",
"message": "The requested connection does not exist."
}
// Connection ended
{
"error_code": "connection_ended",
"message": "This connection has ended and we are no longer listening for events."
}Application State Errors:
// App not recording
{
"error_code": "app_not_recording",
"message": "The requested app is not recording."
}
// Quota exceeded
{
"error_code": "quota_exceeded",
"message": "Quota exceeded."
}- Language: Python 3.8
- Framework: Django
- DB: Postgres
- Cache: Redis (optional)
The main models from this project are:
An organization is a way to group apps and manage access control.
Fields:
id(UUID): Unique identifiername(string): Organization name set by the ownercreated_at(datetime): When the organization was createdis_active(boolean): Whether the organization is active
Relationships:
- Has many
Apps(one-to-many)
An abstraction of an application being monitored by PeerMetrics.
Fields:
id(UUID): Unique identifiername(string): App name set by userapi_key(string): 32-character alphanumeric key used for SDK authenticationdomain(string, optional): Domain restriction for CORS (recommended for security)organization(FK): The organization that owns this appinterval(integer): How often to collect stats in milliseconds (default: 10000)recording(boolean): Whether the app is actively recording metricsdurations_days(JSON): Cache of call durations for billing periodcreated_at(datetime): When the app was created
Relationships:
- Belongs to one
Organization(many-to-one) - Has many
Conferences(one-to-many) - Has many
Participants(one-to-many) - Has many
Events(one-to-many)
A conference represents a WebRTC call where one or more participants are present. It gets created when a participant connects for the first time.
Fields:
id(UUID): Unique identifierconference_id(string): Conference ID set by user (max 64 chars)conference_name(string, optional): Human-readable conference nameconference_info(JSON): Additional conference metadatastart_time(datetime): When the first participant connectedcall_start(datetime): When the current active call startedend_time(datetime): When the last connection closedongoing(boolean): Whether the call is currently activeduration(integer): Total duration of active connections in secondsapp(FK): The app that created this conference
Relationships:
- Belongs to one
App(many-to-one) - Has many
Participants(many-to-many) - Has many
Sessions(one-to-many) - Has many
Connections(one-to-many) - Has many
Events(one-to-many)
A participant is an endpoint (e.g., browser) for which metrics are gathered. A participant is made unique by the combination of app_id:participant_id.
Fields:
id(UUID): Unique identifierparticipant_id(string): Participant ID set by user (max 64 chars)participant_name(string, optional): Human-readable participant nameis_sfu(boolean): Whether this participant is an SFU serverapp(FK): The app that created this participantcreated_at(datetime): When the participant was first seen
Relationships:
- Belongs to one
App(many-to-one) - Participates in many
Conferences(many-to-many) - Has many
Sessions(one-to-many) - Has many
Connections(one-to-many) - Has many
Events(one-to-many)
A session represents a participant's presence in a conference. A participant can have multiple sessions if they rejoin.
Fields:
id(UUID): Unique identifierconference(FK): The conference this session belongs toparticipant(FK): The participant this session belongs toconstraints(JSON): Media constraints used (audio/video settings)devices(JSON): Device information (camera, microphone)platform(JSON): Browser/OS informationmetadata(JSON): Custom metadata (max 5 keys)geo_ip(JSON): Geographic location dataapp_version(string): User's app versionwebrtc_sdk(string): WebRTC SDK being usedsession_info(JSON): Session-specific information and warningsduration(integer): Total session duration in secondscreated_at(datetime): When the session started (start_time)end_time(datetime): When the session endedcall_start(datetime): When the user had an active connection
Relationships:
- Belongs to one
Conference(many-to-one) - Belongs to one
Participant(many-to-one) - Has many
Connections(one-to-many) - Has many
Tracks(one-to-many) - Has many
Events(one-to-many) - Has many
Issues(one-to-many)
A connection represents a WebRTC peer connection between two participants.
Fields:
id(UUID): Unique identifiersession(FK): The session this connection belongs toconference(FK): The conference this connection belongs toparticipant(FK): The local participantpeer(FK): The remote participanttype(string): Connection type (host, srflx, prflx, relay)state(string): Connection state (new, connecting, connected, disconnected, failed, closed)connection_info(JSON): Connection metadata including negotiationsstart_time(datetime): When the connection was createdend_time(datetime): When the connection closedduration(integer): Connection duration in seconds
Relationships:
- Belongs to one
Session(many-to-one) - Belongs to one
Conference(many-to-one) - Belongs to one
Participantas local (many-to-one) - Belongs to one
Participantas peer (many-to-one) - Has many
Tracks(one-to-many) - Has many
Events(one-to-many) - Has many
Issues(one-to-many)
A track represents a media stream (audio or video) in a connection.
Fields:
id(UUID): Unique identifiersession(FK): The session this track belongs toconnection(FK): The connection this track belongs totrack_id(string): Track identifier from WebRTCdirection(string): Track direction (inbound or outbound)kind(string): Track type (audio or video)
Relationships:
- Belongs to one
Session(many-to-one) - Belongs to one
Connection(many-to-one) - Has many
Events(one-to-many, stats events)
This model represents all events saved during a call. Events are differentiated by the category attribute.
Fields:
id(UUID): Unique identifierconference(FK): The conference this event belongs toparticipant(FK): The participant who generated this eventpeer(FK, optional): The remote participant (for connection events)session(FK): The session this event belongs toconnection(FK, optional): The connection this event relates totrack(FK, optional): The track this event relates toapp(FK): The app this event belongs totype(string): Event type (e.g., 'getUserMedia', 'onicecandidate', 'stats')category(string): Event category codedata(JSON): Event-specific datacreated_at(datetime): When the event occurred
Event Categories:
B- Browser events (unload, beforeunload, visibility changes)M- getUserMedia events (camera/microphone access)C- Connection events (ICE, signaling, peer connection events)T- Track events (addTrack, removeTrack)S- Stats events (WebRTC statistics)
Relationships:
- Belongs to one
App(many-to-one) - Belongs to one
Conference(many-to-one) - Belongs to one
Participant(many-to-one) - Optionally belongs to one
Participantas peer (many-to-one) - Belongs to one
Session(many-to-one) - Optionally belongs to one
Connection(many-to-one) - Optionally belongs to one
Track(many-to-one)
The data model follows a hierarchical structure:
Organization
└── App (has api_key)
├── Conference (identified by conference_id)
│ ├── Participants (many-to-many)
│ ├── Sessions
│ │ ├── Connections
│ │ │ └── Tracks
│ │ └── Events
│ └── Events
└── Participants (identified by participant_id)
├── Sessions
└── Events
Key Relationships:
-
Organization → App: One organization can have multiple apps. Each app belongs to one organization.
-
App → Conference: One app can have multiple conferences. Each conference belongs to one app and is identified by a user-provided
conference_id. -
Conference ↔ Participant: Many-to-many relationship. A conference can have multiple participants, and a participant can join multiple conferences.
-
Participant → Session: One participant can have multiple sessions in the same conference (if they rejoin). Each session belongs to one participant and one conference.
-
Session → Connection: One session can have multiple connections (one per peer). Each connection represents a peer-to-peer link.
-
Connection → Track: One connection can have multiple tracks (audio/video, inbound/outbound). Each track belongs to one connection.
-
Events: Events are linked to app, conference, participant, session, and optionally to connection and track, depending on the event type.
Data Flow Example:
When a user joins a call:
- SDK calls
/initializewithapi_key→ validatesApp - Creates or retrieves
Conference(byconference_id) - Creates or retrieves
Participant(byparticipant_id) - Links
ParticipanttoConference - SDK creates
Session→ links toConferenceandParticipant - When peer connection is established → creates
Connection - When media tracks are added → creates
Trackentries - Continuously sends
Events(stats, connection state changes, etc.)
We can group the routes into 2 categories: public (used by the SDK) and private (used by web interface to query data).
Public endpoints are used by the SDK to send metrics and events. They use JWT token authentication.
-
/initialize: First endpoint hit by SDK. Validatesapi_keyand returns an init token.POST- Body:
{ apiKey, conferenceId, conferenceName?, userId, userName? } - Returns:
{ token, getStatsInterval, batchConnectionEvents, time }
-
/sessions: Manage participant sessionsPOST: Create a new session using init token- Body:
{ token, appVersion?, webrtcSdk?, platform?, devices?, constraints?, meta? } - Returns:
{ token }(session token)
- Body:
PUT: Update an existing session using session token- Body:
{ token, constraints?, devices?, platform?, webrtcSdk? }
- Body:
-
/events/get-user-media: getUserMedia events (camera/microphone access)POST- Body:
{ token, data }
-
/events/browser: Browser events (unload, page visibility, etc.)POST- Body:
{ token, type, data }
-
/connection: Connection events (SDP, ICE, peer events)POST- Body:
{ token, type, peerId?, peerName?, connectionId?, data }
-
/connection/batch: Batched connection eventsPOST- Body:
{ token, events: [...] }
-
/stats: WebRTC statisticsPOST- Body:
{ token, connectionId, data, delta? }
Private endpoints are used by the web interface to query data. They require user authentication.
-
/sessions: Query participant sessionsGET, query parameters:conferenceId: Get sessions for a specific conferenceparticipantId: Get sessions for a specific participantappId: Get sessions for a specific app
-
/sessions/<uuid:pk>: Get a specific sessionGET
-
/organizations: Manage organizationsPOST: Create a new organizationGET: List user's organizations
-
/organizations/<uuid:pk>: Manage a specific organizationGET: Get organization detailsPUT: Update organizationDELETE: Delete organization
-
/apps: Manage appsPOST: Create a new appGET: List user's apps
-
/apps/<uuid:pk>: Manage a specific appGET: Get app detailsPUT: Update appDELETE: Delete app
-
/conferences: Query conferencesGET, query parameters:appId: Filter by appparticipantId: Filter by participant
-
/conferences/<uuid:pk>: Get a specific conferenceGET
-
/conferences/<uuid:pk>/events: Get all events for a conferenceGET, query parameters:type: Filter events by type (e.g.,?type=stats)
-
/participants: Query participantsGET, query parameters:appId: Filter by app
-
/participants/<uuid:pk>: Get a specific participantGET
-
/connections: Query connectionsGET
-
/connections/<uuid:pk>: Get a specific connectionGET
-
/issues: Query issues detected during callsGET
-
/issues/<uuid:pk>: Get a specific issueGET
-
/search: Search across resourcesGET, query parameters:query: Search term
The SDK continuously sends metrics to the API server during a WebRTC call:
-
Initialization Phase
- SDK calls
/initializewith API key - API validates the key against the
Appmodel - API creates or retrieves
ConferenceandParticipantrecords - API returns an init token (JWT) valid for 24 hours
- SDK calls
-
Session Creation
- SDK calls
POST /sessionswith init token - API creates a
Sessionrecord with platform, device, and constraint information - API returns a session token (JWT) valid for 24 hours
- Session token is used for all subsequent requests
- SDK calls
-
Event Collection
- SDK sends various events using the session token:
- getUserMedia events: When camera/microphone access is requested
- Connection events: When peer connections are created, ICE candidates are gathered, connection state changes
- Stats events: Periodic WebRTC statistics (every 10 seconds by default)
- Browser events: Page visibility changes, unload events
- Each event is stored as a
GenericEventrecord with appropriate relationships
- SDK sends various events using the session token:
-
Connection Tracking
- When a peer connection is established, a
Connectionrecord is created - Connection state changes are tracked (new → connecting → connected)
- Connection type (host, srflx, relay) is detected from stats
- Negotiation attempts are tracked in
connection_info
- When a peer connection is established, a
-
Track Management
- When media tracks are added,
Trackrecords are created - Tracks are linked to connections and sessions
- Stats events for specific tracks are associated with the
Trackrecord
- When media tracks are added,
-
Session Termination
- When the user leaves, SDK sends an unload event
- API marks all connections as ended
- API calculates session duration
- API updates app usage metrics for billing
Data is organized hierarchically in PostgreSQL:
Organization Level:
- Organizations group multiple apps
- Used for access control and billing
App Level:
- Each app has a unique API key
- Apps track usage metrics in
durations_days(JSON field) - Apps can restrict domains for CORS security
Conference Level:
- Conferences group all data for a single call
- Track start time, end time, duration, and ongoing status
- Store conference-level metadata in
conference_info
Session Level:
- Sessions represent a participant's presence in a conference
- Store platform, device, and constraint information
- Track session duration and connection time
- Store custom metadata (up to 5 key-value pairs)
Connection Level:
- Connections represent peer-to-peer links
- Track connection type (host, srflx, prflx, relay)
- Store negotiation history in
connection_info - Monitor connection state changes
Track Level:
- Tracks represent individual media streams
- Differentiate between audio/video and inbound/outbound
- Link stats events to specific tracks
Event Level:
- All events are stored as
GenericEventrecords - Events are categorized (Browser, Media, Connection, Track, Stats)
- Events maintain relationships to all relevant entities
- Stats events can be cleaned up after conference ends (optional)
Caching:
- Redis is used to cache frequently accessed objects
- Cache keys are generated based on model fields
- Cache TTL is 1 hour by default
- Improves query performance for active conferences
The web interface queries data through private endpoints:
Conference View:
- Query conference details:
GET /conferences/{id} - Query all sessions:
GET /sessions?conferenceId={id} - Query all events:
GET /conferences/{id}/events - Filter events by type:
GET /conferences/{id}/events?type=stats
Participant View:
- Query participant details:
GET /participants/{id} - Query participant sessions:
GET /sessions?participantId={id} - Query conferences:
GET /conferences?participantId={id}
Session View:
- Query session details:
GET /sessions/{id} - Query connections:
GET /connections?sessionId={id} - Query issues:
GET /issues?sessionId={id}
App Dashboard:
- Query all conferences:
GET /conferences?appId={id} - Query all participants:
GET /participants?appId={id} - Query all sessions:
GET /sessions?appId={id}
Data Retention:
- Queries are limited to the last 30 days by default
- Older data can be archived or deleted based on retention policy
- Stats events can be cleaned up after conference ends to save storage
Performance Optimization:
- Indexes on frequently queried fields (
conference_id,participant_id,created_at) - Redis caching for active conferences and sessions
- Connection pooling for database connections (
CONN_MAX_AGE) - Optional batch processing for connection events