Skip to main content

Extension Management

Ominis Cluster Manager provides comprehensive SIP extension management with PostgreSQL-backed storage, auto-reload mechanisms, and full PBX features including call forwarding, DND, voicemail, and call recording.

Overview

Extensions are SIP user accounts that enable authentication and call routing within the FreeSWITCH infrastructure. Unlike queue containers which are ephemeral, extensions are persistent entities stored in PostgreSQL and accessed via mod_xml_curl for dynamic authentication.

Key Features

  • PostgreSQL-Backed Storage: Centralized, persistent extension storage
  • Dynamic Authentication: FreeSWITCH mod_xml_curl integration for real-time lookups
  • Auto-Reload Mechanism: Profile reloads automatically trigger on extension changes
  • Hybrid Authentication:
    • Cluster IPs (10.42.x.x): Blind registration (no auth required)
    • External IPs: mod_xml_curl → API → PostgreSQL authentication
  • Rich PBX Features:
    • Call forwarding (unconditional, busy, no-answer, unavailable)
    • Do Not Disturb (DND)
    • Voicemail with email notifications
    • Call waiting, transfer, and 3-way calling
    • Call recording
    • Music on hold
  • Bulk Operations: Create multiple extensions in a single request
  • Registration Tracking: Query active SIP registrations across all FreeSWITCH instances

Architecture

Storage Backend

Extensions are stored in PostgreSQL with JSONB columns for structured data:

CREATE TABLE extensions (
number VARCHAR(20) PRIMARY KEY,
password VARCHAR(255) NOT NULL,
display_name VARCHAR(255),
email VARCHAR(255),
department VARCHAR(255),
status VARCHAR(20) DEFAULT 'active',
call_forwarding_json JSONB,
voicemail_json JSONB,
features_json JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Why PostgreSQL over XML files?

See ADR: Database vs XML Files for the architectural decision.

Authentication Flow

Auto-Reload Mechanism

When extensions are created, updated, or deleted, the API automatically reloads FreeSWITCH profiles to pick up changes:

Key Points:

  • Profile reload is non-blocking (uses await directory.reload_profile("internal"))
  • Errors during reload are logged but don't fail the operation
  • Reloads happen for create, update, and delete operations
  • Ensures FreeSWITCH always has the latest extension data

Extension Model

Core Fields

class Extension(BaseModel):
number: str # Extension number (e.g., "1001")
password: str # SIP authentication password
display_name: str # Display name for caller ID
status: ExtensionStatus # active, disabled, suspended
email: Optional[str] # Email address
department: Optional[str] # Department
call_forwarding: List[CallForwarding] # Call forwarding rules
voicemail: VoicemailSettings # Voicemail configuration
features: ExtensionFeatures # PBX features
codec_preferences: List[str] # Preferred codecs
max_concurrent_calls: int # Max concurrent calls
caller_id_number: Optional[str] # Outbound caller ID
caller_id_name: Optional[str] # Outbound caller ID name

Extension Status

StatusDescription
activeExtension is active and can register/receive calls
disabledExtension is disabled (cannot register)
suspendedExtension is temporarily suspended

Call Forwarding

Call forwarding supports multiple rules with different trigger conditions:

class CallForwarding(BaseModel):
type: CallForwardType # unconditional, busy, no_answer, unavailable
destination: str # Destination number or SIP URI
enabled: bool # Whether forwarding is enabled
timeout: Optional[int] # Timeout in seconds for no-answer

Forwarding Types:

  • unconditional: All calls forwarded immediately
  • busy: Forward when extension is busy
  • no_answer: Forward after timeout (default: 20 seconds)
  • unavailable: Forward when extension is unreachable

Voicemail Settings

class VoicemailSettings(BaseModel):
enabled: bool # Enable voicemail
pin: Optional[str] # Voicemail PIN
email: Optional[str] # Email for notifications
attach_audio: bool # Attach audio file to email
delete_after_email: bool # Delete voicemail after emailing

Extension Features

class ExtensionFeatures(BaseModel):
call_waiting: bool # Enable call waiting
call_transfer: bool # Enable call transfer
three_way_calling: bool # Enable 3-way calling
do_not_disturb: bool # Do not disturb mode
call_recording: bool # Enable call recording
music_on_hold: bool # Enable music on hold

API Endpoints

List Extensions

Get all SIP extensions with their configuration.

GET /v1/extensions
X-API-Key: your-api-key

Response:

{
"extensions": [
{
"number": "1001",
"password": "secure123",
"display_name": "John Doe",
"status": "active",
"email": "john@company.com",
"department": "Sales",
"call_forwarding": [],
"voicemail": {
"enabled": true,
"pin": "1234",
"email": "john@company.com",
"attach_audio": false,
"delete_after_email": false
},
"features": {
"call_waiting": true,
"call_transfer": true,
"three_way_calling": true,
"do_not_disturb": false,
"call_recording": false,
"music_on_hold": true
},
"codec_preferences": ["PCMU", "PCMA", "G729"],
"max_concurrent_calls": 1,
"caller_id_number": null,
"caller_id_name": null
}
],
"total": 1
}

Create Extension

Create a new SIP extension with authentication credentials and PBX features.

POST /v1/extensions
X-API-Key: your-api-key
Content-Type: application/json

{
"number": "1001",
"password": "secure123",
"display_name": "John Doe",
"email": "john@company.com",
"department": "Sales"
}

Response: 201 Created

{
"extension": {
"number": "1001",
"password": "secure123",
"display_name": "John Doe",
"status": "active",
"email": "john@company.com",
"department": "Sales",
"call_forwarding": [],
"voicemail": {
"enabled": true,
"pin": null,
"email": null,
"attach_audio": false,
"delete_after_email": false
},
"features": {
"call_waiting": true,
"call_transfer": true,
"three_way_calling": true,
"do_not_disturb": false,
"call_recording": false,
"music_on_hold": true
}
}
}

What Happens:

  1. Checks if extension already exists (returns 409 if duplicate)
  2. Stores extension in PostgreSQL
  3. Registers with Kamailio (if enabled)
  4. Reloads FreeSWITCH profile to pick up new user
  5. Updates Prometheus metrics
  6. Returns created extension

Error Codes:

  • 409 Conflict: Extension already exists
  • 500 Internal Server Error: Failed to create extension

Get Extension

Get a specific SIP extension by number.

GET /v1/extensions/1001
X-API-Key: your-api-key

Response:

{
"extension": {
"number": "1001",
"password": "secure123",
"display_name": "John Doe",
"status": "active",
"email": "john@company.com",
"department": "Sales"
}
}

Error Codes:

  • 404 Not Found: Extension not found

Update Extension

Update extension settings including password, display name, and features.

PUT /v1/extensions/1001
X-API-Key: your-api-key
Content-Type: application/json

{
"display_name": "John Smith",
"email": "john.smith@company.com",
"features": {
"do_not_disturb": true,
"call_recording": true
}
}

Response:

{
"extension": {
"number": "1001",
"password": "secure123",
"display_name": "John Smith",
"status": "active",
"email": "john.smith@company.com",
"department": "Sales",
"features": {
"call_waiting": true,
"call_transfer": true,
"three_way_calling": true,
"do_not_disturb": true,
"call_recording": true,
"music_on_hold": true
}
}
}

What Happens:

  1. Fetches current extension from PostgreSQL
  2. Applies partial updates (only provided fields)
  3. Re-registers with Kamailio if password or status changed
  4. Reloads FreeSWITCH profile to pick up changes
  5. Updates Prometheus metrics
  6. Returns updated extension

Error Codes:

  • 404 Not Found: Extension not found
  • 500 Internal Server Error: Failed to update extension

Delete Extension

Remove the extension and clear any active registrations.

DELETE /v1/extensions/1001
X-API-Key: your-api-key

Response: 204 No Content

What Happens:

  1. Checks extension exists (returns 404 if not found)
  2. Removes from PostgreSQL
  3. Best-effort clear of SIP registrations via CLI (kamctl/kamcmd/opensips-cli)
  4. Reloads FreeSWITCH profile to remove user
  5. Updates Prometheus metrics

Error Codes:

  • 404 Not Found: Extension not found
  • 500 Internal Server Error: Failed to delete extension

Enable Extension

Enable a disabled SIP extension.

POST /v1/extensions/1001/enable
X-API-Key: your-api-key

Response:

{
"message": "Extension enabled successfully"
}

Error Codes:

  • 404 Not Found: Extension not found
  • 500 Internal Server Error: Failed to enable extension

Disable Extension

Disable a SIP extension (prevents registration).

POST /v1/extensions/1001/disable
X-API-Key: your-api-key

Response:

{
"message": "Extension disabled successfully"
}

What Happens:

  1. Updates extension status to disabled
  2. Best-effort clear of any active registrations
  3. Updates Prometheus metrics

Error Codes:

  • 404 Not Found: Extension not found
  • 500 Internal Server Error: Failed to disable extension

Change Password

Update the SIP authentication password for an extension.

POST /v1/extensions/1001/password
X-API-Key: your-api-key
Content-Type: application/json

{
"password": "new-secure-password"
}

Response:

{
"message": "Password updated successfully"
}

Error Codes:

  • 404 Not Found: Extension not found
  • 400 Bad Request: Password is required
  • 500 Internal Server Error: Failed to change password

Set Call Forwarding

Configure call forwarding rules for an extension.

POST /v1/extensions/1001/call-forwarding
X-API-Key: your-api-key
Content-Type: application/json

{
"forwarding_rules": [
{
"type": "unconditional",
"destination": "1002",
"enabled": true
},
{
"type": "no_answer",
"destination": "voicemail",
"enabled": true,
"timeout": 30
}
]
}

Response:

{
"message": "Call forwarding updated successfully"
}

Forwarding Types:

  • unconditional: All calls forwarded immediately
  • busy: Forward when extension is busy
  • no_answer: Forward after timeout
  • unavailable: Forward when extension is unreachable

Error Codes:

  • 404 Not Found: Extension not found
  • 500 Internal Server Error: Failed to set call forwarding

Get Call Forwarding

Get call forwarding configuration for an extension.

GET /v1/extensions/1001/call-forwarding
X-API-Key: your-api-key

Response:

{
"call_forwarding": [
{
"type": "unconditional",
"destination": "1002",
"enabled": true,
"timeout": 20
}
]
}

Error Codes:

  • 404 Not Found: Extension not found

Create Bulk Extensions

Create multiple extensions in a single request.

POST /v1/extensions/bulk
X-API-Key: your-api-key
Content-Type: application/json

[
{
"number": "1001",
"password": "pass1",
"display_name": "John Doe",
"email": "john@company.com",
"department": "Sales"
},
{
"number": "1002",
"password": "pass2",
"display_name": "Jane Smith",
"email": "jane@company.com",
"department": "Support"
},
{
"number": "1003",
"password": "pass3",
"display_name": "Bob Wilson",
"email": "bob@company.com",
"department": "Engineering"
}
]

Response:

{
"created": 3,
"total_requested": 3,
"extensions": [
{
"number": "1001",
"display_name": "John Doe",
"status": "active"
},
{
"number": "1002",
"display_name": "Jane Smith",
"status": "active"
},
{
"number": "1003",
"display_name": "Bob Wilson",
"status": "active"
}
],
"errors": []
}

Partial Success: If some extensions fail to create (e.g., duplicates), the response includes an errors array:

{
"created": 2,
"total_requested": 3,
"extensions": [...],
"errors": [
"Extension 1001 already exists"
]
}

Get Active Registrations

Get all active SIP registrations across all FreeSWITCH instances.

GET /v1/registrations
X-API-Key: your-api-key

Response:

{
"registrations": [
{
"number": "1001",
"contact": "sip:1001@192.168.1.100:5060",
"expires": 3600,
"user_agent": null,
"last_registration": null
},
{
"number": "agent-10720",
"contact": "sip:agent-10720@10.42.1.5:5060",
"expires": 3600,
"user_agent": null,
"last_registration": null
}
],
"total": 2
}

Performance Note: This endpoint uses a fast database query to retrieve registrations. FreeSWITCH stores registrations in PostgreSQL, so we query directly rather than using XML-RPC for optimal speed.

Error Codes:

  • 500 Internal Server Error: Failed to get registrations from database

Database Schema

Extensions Table

CREATE TABLE extensions (
-- Primary key
number VARCHAR(20) PRIMARY KEY,

-- Authentication
password VARCHAR(255) NOT NULL,

-- User information
display_name VARCHAR(255),
email VARCHAR(255),
department VARCHAR(255),

-- Status
status VARCHAR(20) DEFAULT 'active'
CHECK (status IN ('active', 'inactive', 'suspended')),

-- JSONB columns for structured data
call_forwarding_json JSONB,
voicemail_json JSONB,
features_json JSONB,

-- Timestamps
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Indexes for common queries
CREATE INDEX idx_extensions_status ON extensions(status);
CREATE INDEX idx_extensions_department ON extensions(department);
CREATE INDEX idx_extensions_email ON extensions(email);

-- Trigger to update updated_at timestamp
CREATE TRIGGER trigger_extensions_updated_at
BEFORE UPDATE ON extensions
FOR EACH ROW
EXECUTE FUNCTION update_extensions_updated_at();

JSONB Column Structure

call_forwarding_json:

[
{
"type": "unconditional",
"destination": "1002",
"enabled": true,
"timeout": 20
}
]

voicemail_json:

{
"enabled": true,
"pin": "1234",
"email": "user@company.com",
"attach_audio": false,
"delete_after_email": false
}

features_json:

{
"call_waiting": true,
"call_transfer": true,
"three_way_calling": true,
"do_not_disturb": false,
"call_recording": false,
"music_on_hold": true
}

Directory Service Integration

mod_xml_curl Configuration

FreeSWITCH queries the API for directory lookups via mod_xml_curl:

FreeSWITCH Configuration:

<configuration name="xml_curl.conf" description="XML CURL Gateway">
<bindings>
<binding name="directory">
<param name="gateway-url"
value="http://cluster-manager-api:8000/v1/freeswitch/directory"
bindings="directory"/>
<param name="gateway-credentials" value="X-FreeSWITCH-Token: your-token"/>
<param name="disable-100-continue" value="true"/>
</binding>
</bindings>
</configuration>

Directory Lookup Endpoint

Endpoint: POST /v1/freeswitch/directory

Called by FreeSWITCH when:

  • External IP attempts to register (not in cluster ACL)
  • External call requires authentication
  • Directory lookup for routing decisions

Request Format:

section=directory
tag_name=domain
key_name=name
key_value=domain.com
user=1001
domain=domain.com
sip_auth_username=1001
sip_auth_realm=domain.com

Response Format:

<document type="freeswitch/xml">
<section name="directory">
<domain name="domain.com">
<user id="1001">
<params>
<param name="password" value="secure123"/>
<param name="dial-string" value="{...}"/>
</params>
<variables>
<variable name="user_context" value="default"/>
<variable name="effective_caller_id_name" value="John Doe"/>
<variable name="effective_caller_id_number" value="1001"/>
</variables>
</user>
</domain>
</section>
</document>

Authentication Security

Token Authentication:

  • Public IPs: Require X-FreeSWITCH-Token header matching FREESWITCH_XMLCURL_TOKEN
  • Private IPs (10.x.x.x, 192.168.x.x, 127.0.0.1): Token validation skipped
  • Invalid token: Returns 418 I'm a teapot (security through obscurity)

Hybrid Authentication Model:

  • Cluster IPs (10.42.x.x): Blind registration (no authentication required)
    • Configured in FreeSWITCH Sofia profile: accept-blind-reg=true
    • Trusted network ACL allows cluster network
  • External IPs: mod_xml_curl → API → PostgreSQL authentication
    • FreeSWITCH queries API for every authentication attempt
    • API validates against PostgreSQL extensions table

Registration Flow

External SIP Client Registration

Cluster Pod Registration

Extension Repository

The ExtensionRepository class provides the data access layer for extensions.

Key Methods

class ExtensionRepository:
async def get_all() -> List[Extension]
async def get_by_number(extension_number: str) -> Optional[Extension]
async def create(extension: Extension) -> Extension
async def update(extension_number: str, update_data: ExtensionUpdate) -> Optional[Extension]
async def delete(extension_number: str) -> bool
async def count() -> int

JSONB Handling

The repository automatically serializes/deserializes JSONB columns:

Create:

row = await conn.fetchrow(
"""
INSERT INTO extensions (
number, password, display_name, email, department, status,
call_forwarding_json, voicemail_json, features_json
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *
""",
extension.number,
extension.password,
extension.display_name,
extension.email,
extension.department,
extension.status.value,
json.dumps([rule.dict() for rule in extension.call_forwarding]),
json.dumps(extension.voicemail.dict()),
json.dumps(extension.features.dict()),
)

Update:

# Fetch current extension
row = await conn.fetchrow(
"SELECT * FROM extensions WHERE number = $1",
extension_number
)
extension = self._to_extension(row)

# Apply partial updates
update_dict = update_data.dict(exclude_unset=True)
for key, value in update_dict.items():
setattr(extension, key, value)

# Save back to database
row = await conn.fetchrow(
"""
UPDATE extensions
SET password = $1, display_name = $2, ...,
call_forwarding_json = $6, voicemail_json = $7, features_json = $8
WHERE number = $9
RETURNING *
""",
...
)

Metrics

Extension operations expose Prometheus metrics for monitoring:

Counter: extensions_operations_total

Tracks extension operations with labels:

  • operation: create, update, delete, enable, disable, change_password, set_call_forwarding, bulk_create
  • extension: Extension number or "multiple" for bulk operations
  • status: success, error, partial (for bulk operations)

Example:

extensions_operations_total.labels(
operation="create",
extension="1001",
status="success"
).inc()

Gauge: active_extensions_gauge

Tracks total number of active extensions:

Example:

total = await extension_repo.count()
active_extensions_gauge.set(total)

ADR: Database vs XML Files for Extensions

Context:

FreeSWITCH traditionally stores user directory in XML files (conf/directory/default/*.xml). For dynamic environments with frequent extension changes, XML files present challenges:

Problems with XML Files:

  1. No Concurrency: File writes must be serialized to avoid corruption
  2. No Atomicity: Partial writes during crashes leave invalid XML
  3. No Transactions: Can't atomically update multiple extensions
  4. No Query Support: Must parse entire directory for simple lookups
  5. Pod Scaling: Each pod needs its own copy, synchronization is complex
  6. Version Control: Difficult to track changes over time

Decision:

Use PostgreSQL with mod_xml_curl for dynamic user authentication:

FeatureXML FilesPostgreSQL + mod_xml_curl
Concurrency❌ Serial writes✅ MVCC transactions
Atomicity❌ Partial writes✅ ACID guarantees
Transactions❌ No support✅ Full ACID
Queries❌ Full XML parse✅ Fast SQL queries
Scalability❌ File sync✅ Centralized storage
History❌ No audit log✅ Timestamps, triggers
JSONB❌ No structured data✅ JSONB queries

Implementation:

  1. Storage: Extensions stored in PostgreSQL with JSONB for structured features
  2. Authentication: FreeSWITCH queries API via mod_xml_curl for each registration
  3. Performance: Database queries are fast (< 5ms for typical extension lookups)
  4. Caching: Consider Redis caching for high-volume scenarios (not implemented)

Trade-offs:

Pros:

  • ✅ Centralized, consistent storage
  • ✅ Fast queries and transactions
  • ✅ Audit trail via timestamps
  • ✅ Structured data with JSONB
  • ✅ Pod-agnostic (no file sync needed)

Cons:

  • ❌ Dependency on API and PostgreSQL availability
  • ❌ Network latency for each auth request (mitigated by LAN speed)
  • ❌ More complex than flat XML files

Consequences:

  • Extensions are database-only (no XML files)
  • FreeSWITCH uses mod_xml_curl for every external authentication
  • Cluster pods use blind registration (no authentication overhead)
  • API must be highly available (database failure = no external registrations)

Examples

Example 1: Create Extension with Features

curl -X POST http://localhost:8000/v1/extensions \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"number": "1001",
"password": "secure123",
"display_name": "John Doe",
"email": "john@company.com",
"department": "Sales"
}'

Example 2: Update Extension with DND

curl -X PUT http://localhost:8000/v1/extensions/1001 \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"features": {
"do_not_disturb": true,
"call_recording": true
}
}'

Example 3: Configure Call Forwarding

# Forward all calls to 1002
curl -X POST http://localhost:8000/v1/extensions/1001/call-forwarding \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"forwarding_rules": [
{
"type": "unconditional",
"destination": "1002",
"enabled": true
}
]
}'

Example 4: Create Bulk Extensions

curl -X POST http://localhost:8000/v1/extensions/bulk \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '[
{
"number": "1001",
"password": "pass1",
"display_name": "John Doe",
"email": "john@company.com",
"department": "Sales"
},
{
"number": "1002",
"password": "pass2",
"display_name": "Jane Smith",
"email": "jane@company.com",
"department": "Support"
},
{
"number": "1003",
"password": "pass3",
"display_name": "Bob Wilson",
"email": "bob@company.com",
"department": "Engineering"
}
]'

Example 5: Get Active Registrations

curl http://localhost:8000/v1/registrations \
-H "X-API-Key: your-api-key"

Example 6: Delete Extension

curl -X DELETE http://localhost:8000/v1/extensions/1001 \
-H "X-API-Key: your-api-key"

Troubleshooting

Extension Can't Register (External Client)

Symptoms:

  • SIP client shows "Registration failed"
  • FreeSWITCH logs show "xml_curl: Error!"

Diagnosis:

  1. Check FreeSWITCH logs for XML-CURL errors:

    kubectl logs -n client-demo-client freeswitch-registrar-xxx | grep -i xml-curl
  2. Verify token configuration:

    # Check API token
    kubectl get secret cluster-manager-config -o jsonpath='{.data.FREESWITCH_XMLCURL_TOKEN}' | base64 -d

    # Check FreeSWITCH token
    kubectl exec -it freeswitch-registrar-xxx -- cat /etc/freeswitch/autoload_configs/xml_curl.conf.xml
  3. Test directory endpoint manually:

    curl -X POST http://localhost:8000/v1/freeswitch/directory \
    -H "X-FreeSWITCH-Token: your-token" \
    -d "user=1001&domain=domain.com&section=directory"

Solutions:

  • ✅ Ensure FREESWITCH_XMLCURL_TOKEN matches in both API and FreeSWITCH
  • ✅ Verify extension exists in PostgreSQL with status='active'
  • ✅ Check API is reachable from FreeSWITCH pod
  • ✅ Review API logs for authentication errors

Extension Can't Register (Cluster Pod)

Symptoms:

  • Queue/IVR pod can't register with registrar
  • FreeSWITCH logs show "Forbidden by ACL"

Diagnosis:

  1. Check pod IP:

    kubectl get pod queue-sales-xxx -o wide
  2. Verify ACL configuration:

    kubectl exec -it freeswitch-registrar-xxx -- fs_cli -x "acl"
  3. Check Sofia profile settings:

    kubectl exec -it freeswitch-registrar-xxx -- fs_cli -x "sofia status profile internal"

Solutions:

  • ✅ Verify pod IP is in cluster_network ACL (10.42.0.0/16)
  • ✅ Check accept-blind-reg=true in Sofia profile
  • ✅ Review FreeSWITCH logs for ACL rejections
  • ✅ Ensure pod network is correctly configured

FreeSWITCH Profile Not Reloading

Symptoms:

  • Created extension can't register
  • Updated extension still uses old configuration
  • No errors in API logs

Diagnosis:

  1. Check if directory service is available:

    # From API pod
    kubectl exec -it cluster-manager-api-xxx -- python -c "
    from core.di import get_directory
    import asyncio

    async def test():
    directory = get_directory()
    result = await directory.reload_profile('internal')
    print(f'Reload result: {result}')

    asyncio.run(test())
    "
  2. Check FreeSWITCH ESL connectivity:

    # Test ESL from API pod
    telnet freeswitch-registrar 8021

Solutions:

  • ✅ Verify FreeSWITCH ESL is listening on port 8021
  • ✅ Check ESL password matches configuration
  • ✅ Ensure directory adapter is properly injected (dependency injection)
  • ✅ Review API logs for reload warnings

Registration Query Timeout

Symptoms:

  • /v1/registrations endpoint times out
  • High database CPU usage

Diagnosis:

  1. Check database query performance:

    SELECT * FROM registrations WHERE expires > EXTRACT(EPOCH FROM NOW());
  2. Check for missing indexes:

    EXPLAIN ANALYZE SELECT * FROM registrations;
  3. Check number of registrations:

    SELECT COUNT(*) FROM registrations;

Solutions:

  • ✅ Add index on expires column if missing
  • ✅ Implement pagination for large result sets
  • ✅ Consider Redis caching for registration queries
  • ✅ Archive old registrations periodically

Best Practices

Security

  1. Use Strong Passwords: Generate passwords with secrets.token_urlsafe(12) or longer
  2. Token Protection: Keep FREESWITCH_XMLCURL_TOKEN secret and rotate regularly
  3. Status Management: Use disabled status instead of deleting extensions
  4. API Key: Always use API key authentication in production

Performance

  1. Bulk Operations: Use /v1/extensions/bulk for creating multiple extensions
  2. Pagination: Implement pagination for large extension lists
  3. Caching: Consider Redis for high-volume registration queries
  4. Indexes: Ensure indexes on status, department, email columns

Operations

  1. Profile Reload: API automatically reloads FreeSWITCH profiles - no manual intervention needed
  2. Registration Cleanup: Use clear_sip_registrations() when deleting extensions
  3. Metrics: Monitor extensions_operations_total for failures
  4. Backup: Regularly backup PostgreSQL extensions table

Development

  1. Testing: Test with both external IPs (auth) and cluster IPs (blind registration)
  2. JSONB Queries: Use PostgreSQL JSONB operators for advanced queries
  3. Error Handling: Profile reload failures are logged but don't fail operations
  4. Idempotency: Create operations check for duplicates and return 409 Conflict