Skip to content

SIP Trunk System ​

Architecture Overview ​

MongoDB is the single source of truth for all SIP trunk configuration. LiveKit SIP trunks are created via the LiveKit SDK and their IDs are stored back in MongoDB. A periodic sync service (every minute) ensures LiveKit state always matches the database recreating missing trunks, correcting config drift, and cleaning up orphans.

SIPTrunk Model Schema ​

typescript
{
  type: "outbound" | "inbound",
  livekitTrunkId: string,         // LiveKit outbound trunk ID
  livekitInboundTrunkId: string,  // LiveKit inbound trunk ID
  dispatchRuleId: string,         // LiveKit dispatch rule ID (inbound only)
  trunkConfig: {
    provider: string,
    numbers: string[],
    address: string,
    username: string,
    password: string,
    // Additional provider-specific fields
  },
  status: "active" | "inactive" | "error"
}

Outbound Trunk Creation Flow ​

  1. User submits trunk configuration (provider, numbers, SIP credentials)
  2. Backend calls LiveKit SDK createSipOutboundTrunk() with the trunk config
  3. LiveKit returns the created trunk with an ID
  4. Backend stores the livekitTrunkId in the MongoDB SIPTrunk document
  5. Phone numbers from the trunk config are auto-registered in the PhoneNumber pool

Inbound Trunk Creation Flow ​

  1. User submits inbound trunk configuration
  2. Backend calls LiveKit SDK createSipInboundTrunk() with numbers and SIP credentials
  3. LiveKit returns the inbound trunk with an ID → stored as livekitInboundTrunkId
  4. Backend immediately creates a dispatch rule linked to the inbound trunk (1:1 relationship)
  5. LiveKit returns the dispatch rule ID → stored as dispatchRuleId
  6. Phone numbers from the trunk config are auto-registered in the PhoneNumber pool

Dispatch Rule Configuration ​

The dispatch rule is created with minimal metadata:

json
{
  "mode": "inbound",
  "need_to_update_agent": true
}

No agent identity is stored in dispatch metadata. Agent resolution happens at call time.

Sync Service ​

Periodic Sync (Every Minute) ​

A cron job runs syncAllTrunks() every 60 seconds. It performs a full bidirectional sync:

  1. Outbound phase: Recreates missing trunks on LiveKit, updates drifted config, deletes orphans, and updates Redis campaign configs when trunks are recreated
  2. Inbound phase: Same logic plus dispatch rule reconciliation (recreates missing rules, deletes orphaned ones)

An overlap lock prevents concurrent sync runs if a previous sync is still in progress.

See PERIODIC-SYNC.md for full details on sync behavior, drift detection, and Redis updates.

Startup Sync ​

On application boot, the same syncAllTrunks() runs once before accepting traffic. This is identical to the periodic sync.

Manual Sync ​

Superadmins can trigger a sync on demand via:

POST /telephony/superadmin/sip-trunks/sync

Trunk Deletion ​

When a trunk is deleted:

  1. LiveKit resources are removed (trunk + dispatch rule for inbound)
  2. The MongoDB SIPTrunk document is deleted
  3. Number cascade: associated phone numbers are cleaned up (removed from pool or unlinked)

LiveKit SDK Interactions ​

OperationSDK Method
Create outbound trunkcreateSipOutboundTrunk()
Create inbound trunkcreateSipInboundTrunk()
Create dispatch rulecreateSipDispatchRule()
Delete outbound trunkdeleteSipTrunk()
Delete inbound trunkdeleteSipTrunk()
Delete dispatch ruledeleteSipDispatchRule()
List trunks (sync)listSipTrunk()

Configuration ​

Environment VariablePurpose
LIVEKIT_URLLiveKit server WebSocket URL
LIVEKIT_API_KEYAPI key for LiveKit SDK authentication
LIVEKIT_API_SECRETAPI secret for LiveKit SDK authentication
AGENT_NAMEName of the Python agent for dispatch

Multi-Provider Support ​

Different SIP providers (Telnyx, Twilio, etc.) work independently. Each trunk stores its own provider-specific configuration in trunkConfig. The system does not enforce a single provider multiple providers can coexist with their own trunks and numbers.

Key Constraints ​

ConstraintEnforcement
One number → one inbound trunkEnforced (unique constraint)
One number → multiple outbound trunksAllowed
One inbound trunk → one dispatch rule1:1 relationship, always created together

Last updated: