VueSip Architecture Documentation
Version: 1.0.0 Last Updated: 2025-11-08 Target Audience: Developers, Technical Architects, Contributors
Note: If you're looking to contribute to VueSip, please also see our Contributing Guide for development workflow, coding standards, and PR process.
Table of Contents
- System Overview
- Headless Pattern Explained
- Layer Architecture
- Component Relationships
- Data Flow Architecture
- State Management
- Plugin Architecture
- WebRTC Integration
- Event System
- Storage Architecture
- Key Design Decisions
- Performance Considerations
- Security Architecture
System Overview
Purpose and Vision
VueSip is a headless Vue.js component library that provides comprehensive SIP (Session Initiation Protocol) and WebRTC functionality for building modern VoIP applications. The library follows a headless UI pattern, completely separating business logic from presentation, allowing developers maximum flexibility in implementing custom user interfaces while leveraging robust communication features.
Core Principles
- Headless Architecture: Zero UI rendering from library components - all functionality exposed via composables
- Type Safety First: Built with TypeScript from the ground up with comprehensive type definitions
- Composable-Driven: Leverages Vue 3 Composition API for maximum flexibility and reusability
- Event-Driven: Comprehensive event system for reactive state management and extensibility
- Production Ready: Includes security, performance optimization, and comprehensive error handling
- Developer Experience: Clear APIs, comprehensive documentation, and extensive testing
Technology Stack
- Vue.js 3.4+: Core framework using Composition API
- TypeScript 5.0+: Type safety and developer experience
- JsSIP 3.10+: SIP protocol implementation (via adapter pattern)
- WebRTC: Native browser APIs for media handling
- Vite 5.0+: Build tool and development server
Note on SIP Library Support: VueSip is designed to support multiple SIP libraries (JsSIP, SIP.js, etc.) through an adapter pattern. Currently, JsSIP is the implemented adapter. See Adapter Architecture and Adapter Roadmap for details on multi-library support.
Headless Pattern Explained
What is a Headless Component?
A headless component provides business logic, state management, and behavior without prescribing any specific UI implementation. In VueSip, this means:
- No DOM Rendering: Components don't render any HTML elements
- Logic Only: All functionality is exposed through JavaScript/TypeScript APIs
- UI Agnostic: Works with any UI framework or custom implementation
- State Exposure: Reactive state is exposed via Vue's reactivity system
- Method Exposure: All operations are available as methods
Benefits of Headless Architecture
- Maximum Flexibility: Developers have complete control over UI/UX
- Framework Agnostic Core: Core logic can potentially be adapted to other frameworks
- Separation of Concerns: Clean separation between business logic and presentation
- Easier Testing: Logic can be tested independently of UI
- Smaller Bundle Size: No CSS or UI component overhead
- Accessibility Control: Developers can implement ARIA and accessibility as needed
Implementation in VueSip
// Headless composable - no UI rendering
export function useCallSession() {
// Internal state (reactive)
const callState = ref<CallState>('idle')
const remoteUri = ref<string | null>(null)
// Exposed methods (business logic)
const makeCall = async (target: string) => {
// Implementation
}
const hangup = async () => {
// Implementation
}
// Return state and methods (no template/rendering)
return {
// Reactive state
callState,
remoteUri,
// Methods
makeCall,
hangup
}
}Developers consume this in their own components:
<template>
<!-- Developer has complete control over UI -->
<div class="my-custom-ui">
<p>Call Status: {{ callState }}</p>
<button @click="makeCall('sip:user@domain')">Call</button>
<button @click="hangup">Hang Up</button>
</div>
</template>
<script setup>
import { useCallSession } from 'vuesip'
// Get state and methods
const { callState, makeCall, hangup } = useCallSession()
</script>Layer Architecture
VueSip follows a four-layer architecture that cleanly separates concerns and establishes clear boundaries between different aspects of the system.
graph TB
subgraph "Integration Layer"
A[Plugins] --> B[Analytics]
A --> C[Recording]
D[Middleware] --> E[Event Interceptors]
F[Hooks] --> G[Lifecycle Hooks]
end
subgraph "Composable Layer"
H[useSipClient] --> I[useSipConnection]
H --> J[useSipRegistration]
K[useCallSession] --> L[useCallControls]
K --> M[useDTMF]
N[useMediaDevices] --> O[useAudioDevices]
P[useCallHistory]
Q[usePresence]
R[useMessaging]
S[useConference]
end
subgraph "Business Logic Layer"
T[SipClient] --> U[Registration]
T --> V[Authentication]
W[CallSession] --> X[Call State Machine]
W --> Y[Media Handling]
Z[MediaManager] --> AA[Device Management]
Z --> AB[Stream Management]
AC[EventBus] --> AD[Event Routing]
AE[Storage] --> AF[Persistence]
end
subgraph "Protocol Layer"
AG[JsSIP] --> AH[SIP Protocol]
AI[WebRTC] --> AJ[RTCPeerConnection]
AI --> AK[MediaStream API]
AL[WebSocket] --> AM[Transport]
AN[STUN/TURN] --> AO[ICE]
end
H -.-> T
K -.-> W
N -.-> Z
T -.-> AG
W -.-> AG
W -.-> AI
Z -.-> AI
T -.-> AL
Z -.-> AN
style A fill:#e1f5ff
style H fill:#fff4e1
style T fill:#ffe1f5
style AG fill:#e1ffe1Layer 1: Protocol Layer
The Protocol Layer handles low-level communication protocols and browser APIs.
Responsibilities:
- SIP protocol implementation via SIP libraries (JsSIP, SIP.js, etc.)
- WebRTC peer connection management
- WebSocket transport for SIP signaling
- ICE candidate gathering via STUN/TURN servers
- Media stream acquisition and management
Key Components:
ISipAdapter: Adapter interface for SIP library abstractionJsSIP UA: User Agent for SIP communication (via JsSipAdapter)RTCPeerConnection: WebRTC connection objectMediaStream API: Browser media accessWebSocket: Signaling transportSTUN/TURN: NAT traversal
Isolation: This layer is completely isolated from Vue and application logic through the adapter pattern. Different SIP libraries can be used by implementing the ISipAdapter interface. See Adapter Architecture for details.
SIP Library Adapters: VueSip uses an adapter pattern to support multiple SIP libraries. Currently implemented: JsSIP. Planned: SIP.js. See Adapter Roadmap.
Layer 2: Business Logic Layer
The Business Logic Layer wraps protocol implementations in application-specific logic.
Responsibilities:
- Call lifecycle management
- Registration and authentication
- Media device management
- Event propagation
- State persistence
- Call history tracking
Key Components:
SipClient (
src/core/SipClient.ts)- Wraps JsSIP User Agent
- Manages SIP registration
- Handles authentication (Digest MD5, HA1)
- Emits SIP-related events
CallSession (
src/core/CallSession.ts)- Manages individual call sessions
- Tracks call state transitions
- Handles call timing and metadata
- Manages media streams for calls
MediaManager (
src/core/MediaManager.ts)- Manages RTCPeerConnection lifecycle
- Handles ICE negotiation
- Manages local and remote media streams
- Enumerates and manages media devices
EventBus (
src/core/EventBus.ts)- Centralized event system
- Type-safe event emission
- Wildcard event support
- Async handler support
TransportManager (
src/core/TransportManager.ts)- WebSocket connection management
- Automatic reconnection with exponential backoff
- Keep-alive mechanism
- Connection state tracking
Design Pattern: Uses the Facade pattern to provide a clean interface to complex protocol implementations while adding application-specific behavior.
Layer 3: Composable Layer
The Composable Layer exposes functionality to Vue applications via composables.
Responsibilities:
- Expose reactive state to components
- Provide methods for SIP operations
- Manage component-level lifecycle
- Integrate with Vue's reactivity system
- Handle dependency injection via provide/inject
Key Composables:
- useSipClient - Main SIP client connection and registration
- useSipConnection - SIP connection state management
- useSipRegistration - Independent registration management
- useCallSession - Individual call session management
- useCallControls - Call control features (hold, mute, transfer)
- useMediaDevices - Media device enumeration and selection
- useDTMF - DTMF tone generation
- useCallHistory - Call history management with persistence
- usePresence - SIP SIMPLE presence
- useMessaging - SIP MESSAGE method
- useConference - Multi-party conferencing
Design Pattern: Follows Vue 3 Composition API patterns with use* naming convention, returning objects with reactive state and methods.
Layer 4: Integration Layer
The Integration Layer provides extensibility and customization points.
Responsibilities:
- Plugin system for extending functionality
- Middleware for event interception
- Lifecycle hooks for custom behavior
- Analytics integration
- Recording and transcription plugins
Key Components:
- PluginManager - Manages plugin lifecycle
- HookManager - Manages lifecycle hooks
- AnalyticsPlugin - Analytics integration
- RecordingPlugin - Call recording
Design Pattern: Uses the Observer pattern for hooks and the Strategy pattern for plugins.
Component Relationships
Dependency Graph
graph TD
subgraph "Application Layer"
APP[Vue Application]
end
subgraph "Provider Components"
SCP[SipClientProvider] --> SC[SipClient Instance]
CFP[ConfigProvider] --> CFG[Configuration]
MDP[MediaProvider] --> MM[MediaManager Instance]
end
subgraph "Composables"
USC[useSipClient]
UCE[useCallSession]
UMD[useMediaDevices]
UCH[useCallHistory]
end
subgraph "Core Classes"
SIP[SipClient]
CS[CallSession]
MED[MediaManager]
EVB[EventBus]
TRM[TransportManager]
end
subgraph "Stores (Pinia)"
CST[callStore]
RST[registrationStore]
DST[deviceStore]
CFST[configStore]
end
subgraph "Storage Adapters"
LSA[LocalStorageAdapter]
SSA[SessionStorageAdapter]
IDB[IndexedDBAdapter]
end
APP --> USC
APP --> UCE
APP --> UMD
USC --> SCP
UCE --> SCP
UCE --> MDP
UMD --> MDP
SCP --> SIP
MDP --> MED
USC --> CST
USC --> RST
UCE --> CST
UMD --> DST
UCH --> LSA
UCH --> IDB
SIP --> EVB
SIP --> TRM
CS --> EVB
CS --> MED
MED --> EVB
CST --> LSA
RST --> LSA
DST --> LSA
CFST --> LSA
style APP fill:#e1f5ff
style USC fill:#fff4e1
style SIP fill:#ffe1f5
style EVB fill:#e1ffe1Component Communication Patterns
1. Provider Pattern
Providers inject global instances into the Vue component tree:
// SipClientProvider creates a singleton SipClient
<SipClientProvider :config="sipConfig">
<App />
</SipClientProvider>Inside components:
// Composable accesses injected instance
const sipClient = inject<SipClient>(SIP_CLIENT_KEY)2. Event-Driven Communication
Components communicate via the EventBus:
// Component A emits event
eventBus.emit('call:incoming', { callId, remoteUri })
// Component B listens
eventBus.on('call:incoming', (payload) => {
// Handle incoming call
})3. Reactive State Sharing
Pinia stores provide reactive global state:
// Store definition
export const useCallStore = defineStore('call', () => {
const activeCalls = ref<Map<string, CallSession>>(new Map())
return { activeCalls }
})
// Usage in composable
const callStore = useCallStore()
callStore.activeCalls.set(callId, session)Dependency Injection Strategy
VueSip uses Vue's provide/inject for dependency injection:
graph TB
ROOT[App Root] --> |provide| SIP[SipClient]
ROOT --> |provide| MED[MediaManager]
ROOT --> |provide| CFG[Config]
ROOT --> |provide| EVT[EventBus]
COMP1[Component 1] --> |inject| SIP
COMP2[Component 2] --> |inject| MED
COMP3[Component 3] --> |inject| CFG
style ROOT fill:#e1f5ff
style COMP1 fill:#fff4e1Benefits:
- Singleton instances shared across application
- No prop drilling
- Easy to mock for testing
- Optional instances (graceful degradation)
Data Flow Architecture
Complete Call Flow
sequenceDiagram
participant User
participant Composable
participant CallSession
participant SipClient
participant JsSIP
participant WebRTC
participant SIPServer
participant RemotePeer
User->>Composable: makeCall('sip:bob@example.com')
Composable->>CallSession: initiate()
CallSession->>WebRTC: getUserMedia()
WebRTC-->>CallSession: localStream
CallSession->>WebRTC: createOffer()
WebRTC-->>CallSession: SDP offer
CallSession->>SipClient: sendInvite(sdp)
SipClient->>JsSIP: call(uri, options)
JsSIP->>SIPServer: INVITE (with SDP)
SIPServer->>RemotePeer: INVITE
RemotePeer-->>SIPServer: 180 Ringing
SIPServer-->>JsSIP: 180 Ringing
JsSIP-->>CallSession: progress event
CallSession-->>Composable: emit('call:ringing')
Composable-->>User: UI shows "Ringing..."
RemotePeer->>RemotePeer: answer()
RemotePeer-->>SIPServer: 200 OK (with SDP)
SIPServer-->>JsSIP: 200 OK
JsSIP-->>CallSession: accepted event
CallSession->>WebRTC: setRemoteDescription(sdp)
CallSession->>JsSIP: sendACK()
JsSIP->>SIPServer: ACK
SIPServer->>RemotePeer: ACK
WebRTC->>WebRTC: ICE negotiation
WebRTC-->>CallSession: connected
CallSession-->>Composable: emit('call:accepted')
Composable-->>User: UI shows "Connected"
Note over WebRTC,RemotePeer: Media flows via WebRTCRegistration Flow
sequenceDiagram
participant App
participant useSipClient
participant SipClient
participant JsSIP
participant SIPServer
App->>useSipClient: connect(config)
useSipClient->>SipClient: start()
SipClient->>JsSIP: new UA(config)
SipClient->>JsSIP: start()
JsSIP->>SIPServer: WebSocket connect
SIPServer-->>JsSIP: Connected
JsSIP-->>SipClient: connected event
SipClient-->>useSipClient: emit('connection:connected')
useSipClient->>SipClient: register()
SipClient->>JsSIP: register()
JsSIP->>SIPServer: REGISTER
SIPServer-->>JsSIP: 401 Unauthorized (challenge)
JsSIP->>JsSIP: calculate digest auth
JsSIP->>SIPServer: REGISTER (with auth)
SIPServer-->>JsSIP: 200 OK
JsSIP-->>SipClient: registered event
SipClient-->>useSipClient: emit('registration:registered')
useSipClient-->>App: isRegistered = trueState Update Flow
graph LR
A[User Action] --> B[Composable Method]
B --> C[Core Class Method]
C --> D[Protocol Layer]
D --> E[State Change]
E --> F[EventBus.emit]
F --> G[Store Update]
G --> H[Vue Reactivity]
H --> I[UI Update]
style A fill:#e1f5ff
style B fill:#fff4e1
style C fill:#ffe1f5
style D fill:#e1ffe1
style I fill:#e1f5ffMedia Stream Flow
graph TB
subgraph "Local Media"
A[getUserMedia] --> B[Local MediaStream]
B --> C[Audio Track]
B --> D[Video Track]
end
subgraph "Peer Connection"
E[RTCPeerConnection] --> F[addTrack]
F --> G[Sender]
end
subgraph "Network"
H[RTP Packets] --> I[SRTP Encryption]
I --> J[Network Transport]
end
subgraph "Remote Media"
K[ontrack event] --> L[Remote MediaStream]
L --> M[Audio Track]
L --> N[Video Track]
end
C --> F
D --> F
G --> H
J --> K
style B fill:#e1f5ff
style E fill:#fff4e1
style J fill:#ffe1f5
style L fill:#e1ffe1State Management
Multi-Layer State Architecture
VueSip uses a multi-layer state management approach:
- Component-Level State: Individual composable state (refs, reactive objects)
- Global Application State: Pinia stores for shared state
- Persistent State: Storage adapters for persistence
graph TB
subgraph "Component State"
A[useCallSession state] --> B[callState: ref]
A --> C[remoteUri: ref]
A --> D[localStream: ref]
end
subgraph "Global State (Pinia)"
E[callStore] --> F[activeCalls: Map]
E --> G[incomingCalls: Array]
H[registrationStore] --> I[isRegistered]
H --> J[registeredUri]
K[deviceStore] --> L[selectedDevices]
K --> M[availableDevices]
end
subgraph "Persistent State"
N[LocalStorage] --> O[User Preferences]
N --> P[Device Selection]
Q[IndexedDB] --> R[Call History]
Q --> S[Recordings]
end
A -.sync.-> E
E -.persist.-> N
E -.persist.-> Q
H -.persist.-> N
K -.persist.-> N
style A fill:#e1f5ff
style E fill:#fff4e1
style N fill:#ffe1f5State Synchronization
Component to Store:
// In composable
const callStore = useCallStore()
watch(callState, (newState) => {
// Update store when component state changes
callStore.updateCallState(callId, newState)
})Store to Persistence:
// In store
const { persist } = usePersistence({
key: 'vuesip:calls',
storage: localStorage,
paths: ['activeCalls', 'callHistory']
})Event-Driven Updates:
// State updates trigger events
eventBus.on('call:stateChanged', ({ callId, state }) => {
callStore.updateCallState(callId, state)
})State Persistence Strategy
LocalStorage (5-10MB limit):
- User preferences (audio/video settings)
- Device selections
- SIP configuration (encrypted)
- Registration state
SessionStorage (5-10MB limit):
- Temporary session data
- Active call metadata
- Connection state
IndexedDB (50MB-500MB+):
- Call history (large datasets)
- Call recordings (binary data)
- Message history
- Presence cache
graph LR
A[Application State] --> B{State Type}
B -->|Preferences| C[LocalStorage]
B -->|Session Data| D[SessionStorage]
B -->|Large Data| E[IndexedDB]
C --> F[Encrypted for Sensitive]
D --> G[Cleared on Close]
E --> H[Indexed Queries]
style A fill:#e1f5ff
style C fill:#fff4e1
style D fill:#ffe1f5
style E fill:#e1ffe1Plugin Architecture
Plugin System Design
VueSip includes a flexible plugin system for extending functionality without modifying core code.
graph TB
subgraph "Plugin Manager"
PM[PluginManager] --> PR[Plugin Registry]
PM --> PL[Plugin Lifecycle]
end
subgraph "Hook System"
HM[HookManager] --> HR[Hook Registry]
HM --> HE[Hook Execution]
end
subgraph "Built-in Plugins"
AP[AnalyticsPlugin] --> AT[Track Events]
RP[RecordingPlugin] --> RC[Record Calls]
end
subgraph "Custom Plugins"
CP[Custom Plugin] --> CH[Custom Hooks]
CP --> CE[Custom Events]
end
PM --> AP
PM --> RP
PM --> CP
HM --> AP
HM --> RP
HM --> CP
style PM fill:#e1f5ff
style HM fill:#fff4e1
style AP fill:#ffe1f5
style CP fill:#e1ffe1Plugin Interface
interface Plugin {
name: string
version: string
install(context: PluginContext): void | Promise<void>
uninstall?(): void | Promise<void>
}
interface PluginContext {
eventBus: EventBus
hooks: HookRegistry
config: Configuration
}Hook System
Available lifecycle hooks:
beforeConnect- Before SIP connectionafterConnect- After successful connectionbeforeRegister- Before SIP registrationafterRegister- After successful registrationbeforeCall- Before initiating a callafterCall- After call connectedbeforeAnswer- Before answering incoming callafterAnswer- After call answeredbeforeHangup- Before call terminationafterHangup- After call terminatedonError- On any error
Hook Execution:
// Register hook
hooks.register('beforeCall', async (context) => {
// Pre-call validation
if (!context.permissions.granted) {
throw new Error('Permissions not granted')
}
})
// Execute hooks
await hooks.execute('beforeCall', context)Analytics Plugin Example
export class AnalyticsPlugin implements Plugin {
name = 'analytics'
version = '1.0.0'
install(context: PluginContext) {
const { eventBus, hooks } = context
// Track call events
eventBus.on('call:*', (event) => {
this.trackEvent('call', event.type, event.data)
})
// Track registration
hooks.register('afterRegister', () => {
this.trackEvent('registration', 'success')
})
}
private trackEvent(category: string, action: string, data?: any) {
// Send to analytics service
console.log(`[Analytics] ${category}:${action}`, data)
}
}Recording Plugin Example
export class RecordingPlugin implements Plugin {
name = 'recording'
version = '1.0.0'
private recorder: MediaRecorder | null = null
install(context: PluginContext) {
const { eventBus } = context
eventBus.on('call:accepted', ({ session }) => {
if (session.remoteStream) {
this.startRecording(session.remoteStream)
}
})
eventBus.on('call:terminated', () => {
this.stopRecording()
})
}
private startRecording(stream: MediaStream) {
this.recorder = new MediaRecorder(stream)
this.recorder.start()
}
private stopRecording() {
if (this.recorder && this.recorder.state !== 'inactive') {
this.recorder.stop()
}
}
}WebRTC Integration
WebRTC Architecture
graph TB
subgraph "Application Layer"
A[useCallSession]
end
subgraph "MediaManager"
B[MediaManager] --> C[Device Management]
B --> D[Stream Management]
B --> E[Connection Management]
end
subgraph "WebRTC APIs"
F[getUserMedia] --> G[Local Streams]
H[RTCPeerConnection] --> I[ICE Handling]
H --> J[SDP Negotiation]
H --> K[DTMF Sender]
L[getStats] --> M[Quality Metrics]
end
subgraph "Network"
N[STUN Servers] --> O[NAT Traversal]
P[TURN Servers] --> Q[Relay]
R[ICE Candidates] --> S[Connectivity]
end
A --> B
B --> F
B --> H
B --> L
H --> N
H --> P
style A fill:#e1f5ff
style B fill:#fff4e1
style H fill:#ffe1f5
style N fill:#e1ffe1ICE Negotiation Flow
sequenceDiagram
participant PC as RTCPeerConnection
participant STUN as STUN Server
participant TURN as TURN Server
participant MM as MediaManager
participant Remote as Remote Peer
PC->>PC: Gather host candidates
PC->>STUN: STUN request
STUN-->>PC: Server reflexive candidate
PC->>TURN: Allocate request
TURN-->>PC: Relay candidate
PC->>MM: onicecandidate
MM->>Remote: Send candidate (via SIP)
Remote->>MM: Receive remote candidate
MM->>PC: addIceCandidate()
PC->>PC: ICE connectivity checks
PC->>Remote: STUN checks
Remote-->>PC: STUN responses
PC->>MM: oniceconnectionstatechange('connected')
MM->>MM: Media flowingMedia Constraints and Quality
Default Audio Constraints:
{
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
sampleRate: 48000,
channelCount: 1
}
}Default Video Constraints:
{
video: {
width: { ideal: 640, max: 1280 },
height: { ideal: 480, max: 720 },
frameRate: { ideal: 30, max: 30 },
facingMode: 'user'
}
}Codec Preference
Audio Codecs (priority order):
- Opus (48 kHz, variable bitrate) - Preferred
- G.722 (16 kHz wideband)
- PCMU (G.711 µ-law, 8 kHz)
- PCMA (G.711 A-law, 8 kHz)
Video Codecs (priority order):
- H.264 (if available) - Preferred
- VP9
- VP8 (fallback)
Statistics Collection
graph LR
A[RTCPeerConnection] --> B[getStats API]
B --> C[Parse Stats]
C --> D[Audio Stats]
C --> E[Video Stats]
C --> F[Network Stats]
D --> G[Bitrate, Packets Lost, Jitter]
E --> H[Frame Rate, Resolution]
F --> I[RTT, Bandwidth]
G --> J[Quality Monitoring]
H --> J
I --> J
J --> K[Adaptive Quality]
style A fill:#e1f5ff
style B fill:#fff4e1
style J fill:#ffe1f5Quality Adaptation:
- Monitor packet loss > 5%: Reduce bitrate
- Monitor RTT > 300ms: Reduce resolution/framerate
- Monitor available bandwidth: Adjust codec parameters
Event System
EventBus Architecture
graph TB
subgraph "Event Emitters"
A[SipClient] --> E[EventBus]
B[CallSession] --> E
C[MediaManager] --> E
D[Stores] --> E
end
subgraph "EventBus"
E --> F[Event Registry]
E --> G[Handler Registry]
E --> H[Wildcard Support]
E --> I[Priority Queue]
end
subgraph "Event Listeners"
J[Composables] --> E
K[Plugins] --> E
L[Application] --> E
end
style E fill:#e1f5ff
style F fill:#fff4e1Event Categories
Connection Events:
connection:connectingconnection:connectedconnection:disconnectedconnection:failedconnection:reconnecting
Registration Events:
registration:registeringregistration:registeredregistration:unregisteredregistration:failedregistration:expiring
Call Events:
call:incomingcall:outgoingcall:ringingcall:progresscall:acceptedcall:answeredcall:heldcall:unheldcall:mutedcall:unmutedcall:terminatedcall:failed
Media Events:
media:deviceChangedmedia:deviceAddedmedia:deviceRemovedmedia:streamAddedmedia:streamRemovedmedia:trackAddedmedia:trackRemoved
Event Payload Structure
interface EventPayload<T> {
type: string // Event type (e.g., 'call:incoming')
timestamp: Date // When event occurred
data: T // Event-specific data
metadata?: Record<string, unknown> // Optional metadata
}Wildcard Event Listening
// Listen to all call events
eventBus.on('call:*', (event) => {
console.log('Call event:', event.type)
})
// Listen to all events
eventBus.on('*', (event) => {
console.log('Any event:', event.type)
})Async Event Handlers
eventBus.on('call:incoming', async (event) => {
// Async operations supported
await checkUserAvailability()
await logIncomingCall(event.data)
})Event Priority
// High priority handler (executes first)
eventBus.on('call:incoming', handler1, { priority: 10 })
// Normal priority (executes later)
eventBus.on('call:incoming', handler2, { priority: 0 })Storage Architecture
Storage Layer Design
graph TB
subgraph "Application"
A[Composables] --> B[Stores]
end
subgraph "Persistence Layer"
C[Persistence Manager] --> D[Storage Strategy]
end
subgraph "Storage Adapters"
E[LocalStorageAdapter]
F[SessionStorageAdapter]
G[IndexedDBAdapter]
H[Custom Adapter]
end
B --> C
D --> E
D --> F
D --> G
D --> H
style A fill:#e1f5ff
style C fill:#fff4e1
style E fill:#ffe1f5Storage Adapter Interface
interface StorageAdapter {
get<T>(key: string): Promise<T | null>
set<T>(key: string, value: T): Promise<void>
remove(key: string): Promise<void>
clear(): Promise<void>
has(key: string): Promise<boolean>
}LocalStorage Adapter
Use Cases:
- User preferences
- Device selections
- SIP configuration (encrypted)
- UI state
Implementation:
export class LocalStorageAdapter implements StorageAdapter {
async get<T>(key: string): Promise<T | null> {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : null
}
async set<T>(key: string, value: T): Promise<void> {
localStorage.setItem(key, JSON.stringify(value))
}
}IndexedDB Adapter
Use Cases:
- Call history (large datasets)
- Call recordings (binary data)
- Message history
- Offline data cache
Schema:
// Database: vuesip
// Version: 1
// Object Store: callHistory
{
keyPath: 'id',
indexes: [
{ name: 'remoteUri', keyPath: 'remoteUri' },
{ name: 'startTime', keyPath: 'startTime' },
{ name: 'direction', keyPath: 'direction' }
]
}
// Object Store: recordings
{
keyPath: 'id',
indexes: [
{ name: 'callId', keyPath: 'callId' },
{ name: 'timestamp', keyPath: 'timestamp' }
]
}Encryption for Sensitive Data
// Encrypt SIP credentials before storage
const encrypted = await encrypt(credentials, userKey)
await storage.set('vuesip:credentials', encrypted)
// Decrypt on retrieval
const encrypted = await storage.get('vuesip:credentials')
const credentials = await decrypt(encrypted, userKey)Encryption Method:
- Web Crypto API (SubtleCrypto)
- AES-GCM algorithm
- PBKDF2 key derivation
- Random IV per encryption
Key Design Decisions
1. Headless Architecture Choice
Decision: Build as headless library with zero UI components
Rationale:
- Maximum flexibility for developers
- Smaller bundle size (no CSS/UI overhead)
- Easier to maintain (no UI testing)
- Framework agnostic core logic
- Better separation of concerns
Trade-offs:
- Higher barrier to entry for beginners
- No ready-to-use UI components
- More work for developers initially
2. Vue 3 Composition API
Decision: Use Composition API exclusively (no Options API)
Rationale:
- Better TypeScript support
- More flexible code organization
- Easier code reuse
- Better tree-shaking
- Aligns with Vue 3 best practices
Trade-offs:
- Requires Vue 3.4+
- Learning curve for Vue 2 developers
- No Options API compatibility
3. JsSIP as SIP Library (Current Default)
Decision: Use JsSIP as primary SIP implementation with adapter pattern for library flexibility
Rationale:
- Mature and battle-tested
- Active maintenance
- Good WebRTC integration
- Comprehensive SIP support
- Browser-native implementation
Alternatives Considered:
- SIP.js (more complex, larger bundle) - Supported via adapter pattern
- Custom implementation (too much effort)
Future Support: VueSip uses an adapter pattern (see Adapter Architecture) that allows runtime selection of SIP libraries. JsSIP is currently the default and only implemented adapter, but SIP.js support is planned. This provides library flexibility without changing application code.
4. Event-Driven Architecture
Decision: Centralized EventBus for all events
Rationale:
- Loose coupling between components
- Easy to extend with plugins
- Clear event flow
- Support for middleware
- Better debugging
Trade-offs:
- More memory usage (event listeners)
- Potential for memory leaks if not cleaned up
- Less explicit than direct method calls
5. Multi-Layer State Management
Decision: Component state + Pinia stores + Persistence
Rationale:
- Component state for local concerns
- Global state for shared data
- Persistence for durability
- Vue reactivity throughout
Alternatives Considered:
- Vue's provide/inject only (not reactive enough)
- Vuex (deprecated in favor of Pinia)
- External state management (breaks Vue integration)
6. TypeScript First
Decision: Built entirely in TypeScript with strict mode
Rationale:
- Better developer experience
- Catch errors at compile time
- Self-documenting code
- Better IDE support
- Industry standard
Trade-offs:
- Larger development overhead
- Compilation required
- Generic complexity
7. Plugin System
Decision: Extensible plugin architecture with hooks
Rationale:
- Core stays focused and small
- Easy to add features without modifying core
- Community can contribute plugins
- Better testability
Alternatives Considered:
- Monolithic design (harder to maintain)
- Inheritance-based (less flexible)
Performance Considerations
Bundle Size Optimization
Targets:
- Minified: < 150 KB
- Gzipped: < 50 KB
Techniques:
- Tree-shaking (ESM exports)
- Code splitting (dynamic imports)
- Lazy loading optional features
- Minimal dependencies
- Terser minification
Runtime Performance
Targets:
- Call setup: < 2 seconds
- State update: < 50ms
- Event propagation: < 10ms
- Memory per call: < 50 MB
Optimizations:
- Object pooling for frequent allocations
- Debounce/throttle for high-frequency events
- Virtual scrolling for call history
- Lazy initialization of heavy objects
- WeakMap for metadata storage
Memory Management
Strategies:
- Automatic cleanup on call termination
- Event listener removal on unmount
- Stream track disposal
- PeerConnection closure
- Store cleanup for terminated calls
Memory Leak Prevention:
// Always clean up in onUnmounted
onUnmounted(() => {
// Remove event listeners
eventBus.off('call:*', handler)
// Stop media tracks
localStream?.getTracks().forEach(track => track.stop())
// Close peer connection
peerConnection?.close()
})Network Optimization
Techniques:
- Connection pooling (single WebSocket)
- Keep-alive to prevent reconnections
- Compression support
- Adaptive bitrate for media
- ICE candidate optimization
Security Architecture
Transport Security
Requirements:
- WSS (WebSocket Secure) mandatory for production
- TLS 1.2 minimum, TLS 1.3 preferred
- Certificate validation
- DTLS-SRTP for media encryption
Authentication
SIP Digest Authentication:
graph LR
A[Client] --> B[REGISTER]
B --> C[Server]
C --> D[401 + Challenge]
D --> A
A --> E[Calculate MD5 Hash]
E --> F[REGISTER + Auth]
F --> C
C --> G[200 OK]Supported Methods:
- Digest Authentication (MD5)
- HA1 hash support
- Authorization username override
- Realm handling
Credential Storage
Best Practices:
- Never store passwords in plain text
- Use Web Crypto API for encryption
- PBKDF2 for key derivation
- Random IV per encryption
- Clear credentials on logout
// Encrypt credentials before storage
const key = await deriveKey(userPassword)
const encrypted = await encrypt(credentials, key)
localStorage.setItem('vuesip:creds', encrypted)Input Validation
Validation Points:
- SIP URI format validation
- Phone number validation
- Header injection prevention
- XSS prevention in display names
- Path traversal prevention
Content Security Policy
Recommended CSP:
default-src 'self';
connect-src 'self' wss://sip.example.com;
media-src 'self' blob:;
script-src 'self';Conclusion
VueSip's architecture is designed for:
- Flexibility: Headless pattern gives developers complete UI control
- Maintainability: Clean layer separation and clear responsibilities
- Extensibility: Plugin system allows customization without core modifications
- Performance: Optimized for production use with careful resource management
- Security: Built-in security best practices and encryption
- Developer Experience: TypeScript-first with comprehensive types and documentation
The four-layer architecture (Protocol → Business Logic → Composable → Integration) provides clear separation of concerns while the event-driven design enables loose coupling and extensibility. This foundation supports building robust, production-ready VoIP applications with Vue.js.
Additional Resources
- Adapter Architecture - Multi-library SIP support design
- Adapter Roadmap - Implementation plan for SIP library adapters
- Technical Specifications:
/TECHNICAL_SPECIFICATIONS.md - Contributing Guide:
/CONTRIBUTING.md- Developer guidelines and workflow - API Documentation:
/docs/api/ - User Guides:
/docs/guide/ - Testing Guide:
/docs/testing-guide.md - Examples:
/examples/and/playground/
Document Revision: 1.0.0 Authors: VueSip Development Team Last Review: 2025-11-08