SMS-C Configuration Reference
← Back to Documentation Index | Main README
Complete reference for all SMS-C configuration options with examples for common deployment scenarios.
Table of Contents
- Configuration Files
- Database Configuration
- API Configuration
- Web UI Configuration
- Federation Configuration
- Message Queue Configuration
- Charging Configuration
- Diameter Sh (HSS) Configuration
- ENUM Configuration
- Number Translation Configuration
- Routing Configuration
- Performance Tuning Configuration
- Logging Configuration
- Common Configuration Scenarios
Configuration Files
The SMS-C uses three main configuration files:
config/config.exs
Static configuration loaded at compile time. Contains:
- Application-wide defaults
- Logger configuration
- Development/test settings
- Performance tuning parameters
config/runtime.exs
Runtime configuration loaded at startup. Contains:
- Database connection settings
- Cluster configuration
- External service integration (OCS, ENUM)
- Initial routes and translation rules
- Environment-specific settings
config/prod.exs (optional)
Production-specific overrides.
Best Practice: Use environment variables in runtime.exs for sensitive values like passwords and API keys.
SQL CDR Storage Configuration
The SMS-C uses Mnesia for operational data (message queue, routing rules, number translations) and supports external SQL databases for long-term CDR (Call Detail Record) storage, billing, and analytics.
Supported SQL Databases
The system supports the following SQL databases for CDR export:
| Database | Version | Adapter | Default Port | Best For |
|---|---|---|---|---|
| MySQL | 8.0+ | Ecto.Adapters.MyXQL | 3306 | General purpose, proven reliability |
| MariaDB | 10.5+ | Ecto.Adapters.MyXQL | 3306 | MySQL-compatible, open source |
| PostgreSQL | 13+ | Ecto.Adapters.Postgres | 5432 | Advanced features, JSON support |
Note: Mnesia is used automatically for operational data (message queue, routing, translations) and requires no configuration. The SQL database is only used for CDR export and long-term storage.
MySQL / MariaDB Configuration
# config/runtime.exs
config :sms_c, SmsC.Repo,
adapter: Ecto.Adapters.MyXQL,
username: System.get_env("DB_USERNAME") || "sms_user",
password: System.get_env("DB_PASSWORD") || "secure_password",
hostname: System.get_env("DB_HOSTNAME") || "localhost",
port: String.to_integer(System.get_env("DB_PORT") || "3306"),
database: System.get_env("DB_NAME") || "sms_c_prod",
pool_size: String.to_integer(System.get_env("DB_POOL_SIZE") || "20")
PostgreSQL Configuration
# config/runtime.exs
config :sms_c, SmsC.Repo,
adapter: Ecto.Adapters.Postgres,
username: System.get_env("DB_USERNAME") || "sms_user",
password: System.get_env("DB_PASSWORD") || "secure_password",
hostname: System.get_env("DB_HOSTNAME") || "localhost",
port: String.to_integer(System.get_env("DB_PORT") || "5432"),
database: System.get_env("DB_NAME") || "sms_c_prod",
pool_size: String.to_integer(System.get_env("DB_POOL_SIZE") || "20")
Choosing a SQL Database
MySQL/MariaDB - Recommended for most deployments:
- Excellent performance for CDR writes
- Proven reliability in telecom environments
- Wide tooling support for billing systems
- Easy replication setup
PostgreSQL - Consider if you need:
- Advanced JSON/JSONB features for analytics
- Complex queries on CDR data
- Existing PostgreSQL infrastructure
- PostGIS for geographic analysis
Deployment Topologies
Important: The SQL CDR database can run on a separate server from your SMS-C instance(s). This is the recommended approach for production deployments.
Single-Server Deployment (Development/Testing):
┌─────────────────────────────┐
│ SMS-C Server │
│ ┌──────────┐ ┌──────────┐ │
│ │ SMS-C │ │ SQL DB │ │
│ │ Instance │ │ (CDR) │ │
│ └──────────┘ └──────────┘ │
│ ┌─────────────────────┐ │
│ │ Mnesia (Operational) │ │
│ └─────────────────────┘ │
└─────────────────────────────┘
Distributed Deployment (Production - Recommended):
┌─────────────────┐ ┌─────────────────┐
│ SMS-C Node 1 │ │ SMS-C Node 2 │
│ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ SMS-C │ │ │ │ SMS-C │ │
│ │ Instance │◄├──────┤►│ Instance │ │
│ └──────┬──────┘ │ │ └──────┬──────┘ │
│ │ │ │ │ │
│ ┌──────▼──────┐ │ │ ┌──────▼──────┐ │
│ │ Mnesia │◄├──────┤►│ Mnesia │ │
│ │(Replicated) │ │ │ │(Replicated) │ │
│ └─────────────┘ │ │ └─────────────┘ │
└────────┬────────┘ └────────┬────────┘
│ │
└──────────┬─────────────┘
│ Network
┌──────────▼────────────┐
│ Dedicated SQL Server │
│ ┌──────────────────┐ │
│ │ MySQL/MariaDB │ │
│ │ or PostgreSQL │ │
│ │ (CDR Storage) │ │
│ └──────────────────┘ │
└───────────────────────┘
Benefits of Separate SQL Server:
- Performance Isolation: CDR writes don't impact message processing
- Scalability: Independently scale database and message processing
- Reliability: Database maintenance doesn't affect SMS-C uptime
- Data Management: Centralized CDR storage for multiple SMS-C instances
- Backup Flexibility: Independent backup schedules and retention policies
Pool Size Guidelines
| Workload | Pool Size | Description |
|---|---|---|
| Development | 5-10 | Minimal concurrency |
| Low Volume (< 100 msg/sec) | 10-15 | Small deployments |
| Medium Volume (100-1000 msg/sec) | 20-30 | Typical production |
| High Volume ( > 1000 msg/sec) | 40-100 | High-throughput scenarios |
Calculation: pool_size = (expected concurrent DB operations) * 1.5
Database Connection Examples
Using Environment Variables (Recommended for Production):
# Set environment variables
export DB_USERNAME=sms_prod_user
export DB_PASSWORD=strong_password_here
export DB_HOSTNAME=db-primary.internal.example.com
export DB_PORT=3306
export DB_NAME=sms_c_production
export DB_POOL_SIZE=30
Direct Configuration (Development Only):
config :sms_c, SmsC.Repo,
username: "dev_user",
password: "dev_password",
hostname: "localhost",
database: "sms_c_dev",
pool_size: 5
Connection Pool Monitoring
Monitor pool usage via Prometheus metrics:
ecto_pools_queue_time- Time waiting for connectionecto_pools_query_time- Query execution timeecto_pools_connected_count- Active connections
Alert if wait time exceeds 100ms consistently - indicates need for larger pool.
API Configuration
The REST API provides message submission and management capabilities.
Basic API Configuration
# config/runtime.exs
config :api_ex,
port: String.to_integer(System.get_env("API_PORT") || "8443"),
listen_ip: System.get_env("API_LISTEN_IP") || "0.0.0.0",
enable_tls: System.get_env("API_ENABLE_TLS") != "false"
TLS/SSL Configuration
Production Setup with TLS (Recommended):
config :api_ex,
port: 8443,
listen_ip: "0.0.0.0",
enable_tls: true,
tls_cert_path: "/etc/sms_c/certs/server.crt",
tls_key_path: "/etc/sms_c/certs/server.key"
Development Setup without TLS:
config :api_ex,
port: 8080,
listen_ip: "127.0.0.1",
enable_tls: false
API Certificate Setup
Generate self-signed certificate for testing:
# Create certificate directory
mkdir -p priv/cert
# Generate private key
openssl genrsa -out priv/cert/server.key 2048
# Generate certificate signing request
openssl req -new -key priv/cert/server.key -out priv/cert/server.csr \
-subj "/C=US/ST=State/L=City/O=Organization/CN=sms-api.example.com"
# Generate self-signed certificate (valid 365 days)
openssl x509 -req -days 365 -in priv/cert/server.csr \
-signkey priv/cert/server.key -out priv/cert/server.crt
# Set permissions
chmod 600 priv/cert/server.key
chmod 644 priv/cert/server.crt
For production, use certificates from a trusted CA (Let's Encrypt, commercial CA, etc.).
API Access Control
IP Whitelisting (Application Firewall):
# Using iptables (Linux)
iptables -A INPUT -p tcp --dport 8443 -s 10.0.0.0/8 -j ACCEPT
iptables -A INPUT -p tcp --dport 8443 -j DROP
# Using firewalld (Red Hat/CentOS)
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.0/8" port protocol="tcp" port="8443" accept'
firewall-cmd --reload
API Key Authentication (Application Level):
Configure via custom plug in router - see Operations Guide for implementation details.
Web UI Configuration
The web interface provides route management, message browsing, and monitoring.
Basic Web UI Configuration
# config/runtime.exs
config :control_panel,
port: String.to_integer(System.get_env("WEB_PORT") || "80"),
hostname: System.get_env("WEB_HOSTNAME") || "localhost",
enable_tls: System.get_env("WEB_ENABLE_TLS") == "true"
Production Web UI Setup
config :control_panel,
port: 443,
hostname: "sms-admin.example.com",
enable_tls: true,
tls_cert_path: "/etc/sms_c/certs/web.crt",
tls_key_path: "/etc/sms_c/certs/web.key"
Reverse Proxy Setup (Recommended)
Use Nginx or Apache as reverse proxy for additional security and features:
Nginx Configuration Example:
upstream sms_web {
server 127.0.0.1:4000;
keepalive 32;
}
server {
listen 80;
server_name sms-admin.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name sms-admin.example.com;
ssl_certificate /etc/letsencrypt/live/sms-admin.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sms-admin.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Basic auth for additional security
auth_basic "SMS-C Admin";
auth_basic_user_file /etc/nginx/.htpasswd;
location / {
proxy_pass http://sms_web;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# WebSocket support for LiveView
location /live {
proxy_pass http://sms_web;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
}
Federation Configuration
OmniMessage uses HTTP-based federation for multi-controller deployments. Controllers discover each other via DNS SRV records or static peer lists, exchange health and frontend registries over HTTPS, and forward messages to remote controllers when needed.
See the Geographic Federation Guide for full architecture, deployment examples, and troubleshooting.
Quick Start
# config/runtime.exs
config :sms_c, :federation,
enabled: true,
dns_srv_domain: "_smsc._tcp.smsc.example.com"
Full Parameter Reference
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | false | Master switch for federation. |
dns_srv_domain | String | "" | DNS SRV domain for peer discovery. |
dns_poll_interval_ms | Integer | 30000 | DNS re-resolution interval. |
health_check_interval_ms | Integer | 10000 | Peer health check interval. |
registry_sync_interval_ms | Integer | 15000 | Frontend registry exchange interval. |
forward_retry_interval_ms | Integer | 5000 | Queued message retry interval. |
forward_max_retries | Integer | 50 | Max forward attempts before marking failed. |
http_timeout_ms | Integer | 5000 | Timeout for all peer HTTP calls. |
api_port | Integer | 8443 | API port for constructing peer URLs. |
static_peers | List | [] | Static peer list (alternative to DNS SRV). |
Network Requirements
Federation requires only port 8443 (HTTPS) between sites, compared to ports 4369 + 9100-9200 for Erlang clustering.
# Allow federation traffic from peer sites
iptables -A INPUT -p tcp -s 10.0.1.0/24 --dport 8443 -j ACCEPT
iptables -A INPUT -p tcp -s 10.0.2.0/24 --dport 8443 -j ACCEPT
Message Queue Configuration
Controls message retention and expiration behavior.
Message Expiration
# config/runtime.exs
config :sms_c,
dead_letter_time_minutes: 1440 # 24 hours
Common Values:
- 60 - 1 hour (testing/development)
- 1440 - 24 hours (typical production)
- 4320 - 3 days (extended retention)
- 10080 - 7 days (maximum retention)
Messages older than this value become undeliverable and are marked for cleanup.
Delivery Retry Configuration
Retry behavior uses exponential backoff:
Retry Delay = 2^(attempt_count) minutes
| Attempt | Delay |
|---|---|
| 1 | 2 minutes |
| 2 | 4 minutes |
| 3 | 8 minutes |
| 4 | 16 minutes |
| 5 | 32 minutes |
| 6 | 64 minutes |
| 7 | 128 minutes |
| 8 | 256 minutes |
Maximum attempts before dead letter: Limited by dead_letter_time_minutes.
Cleanup Configuration
# config/config.exs
config :sms_c,
cleanup_interval_minutes: 10,
fingerprint_ttl_minutes: 5,
event_ttl_days: 7
Cleanup Intervals:
- cleanup_interval_minutes: How often cleanup worker runs (default: 10)
- fingerprint_ttl_minutes: Duplicate detection window (default: 5)
- event_ttl_days: Event log retention (default: 7)
Charging Configuration
Online charging is performed over the Diameter Ro interface (Credit-Control
Application, RFC 4006). For each charged message the SMS-C sends a
Credit-Control-Request (Event-Request, CC-Request-Type 4) to the OCS for the
message sender; the OCS authorizes or declines based on the subscriber's
balance. This reuses the Diameter stack (see
Diameter Sh below) — the :ro application
must be registered.
Enable Charging
# config/runtime.exs
config :sms_c,
default_charging_enabled: true
config :sms_c, :charging,
# What to do when the sender is out of balance:
# :reject - mark the message :balance_rejected and do not deliver it
# :auto_reply - additionally SMS out_of_balance_reply_text back to the sender
out_of_balance_action: :reject,
out_of_balance_reply_text: "Your message could not be sent due to insufficient balance.",
# Fail mode when the OCS errors or the Diameter stack is not configured
# (NOT a definitive out-of-balance answer):
# :allow - fail open, deliver the message (default)
# :reject - fail closed, treat as out of balance
on_charging_error: :allow,
# Rating profile selecting how the OCS rates the message. Sent as the CCR
# Service-Context-Id.
rating_profile: "sms@sms-c"
Charging also requires the Diameter stack enabled with the :ro application
(Credit-Control is IETF base, vendor 0):
config :sms_c,
diameter_enabled: true
config :diameter_ex,
diameter: %{
service_name: :omnimessage,
host: "smsc01",
realm: "epc.mnc005.mcc547.3gppnetwork.org",
# ... see Diameter Sh section for the full stack/peers ...
applications: [
%{
application_name: :ro,
application_dictionary: :diameter_gen_3gpp_ro,
vendor_specific_application_ids: [
%{vendor_id: 0, auth_application_id: 4, acct_application_id: nil}
]
}
]
}
Per-Route Charging
Each route's charged policy controls charging independently of the global
default:
charged | Behaviour |
|---|---|
:yes | Always balance-check via the OCS |
:no | Never charge |
:default | Use default_charging_enabled |
Disable Charging
config :sms_c,
default_charging_enabled: false
When disabled (the default), :default routes route without any OCS balance
check. Individual routes can still opt in with charged: :yes.
Defaults & Graceful Degradation
The config :sms_c, :charging block is optional — every key has a sensible
default (out_of_balance_action: :reject, a built-in reply text, and
rating_profile: "sms@sms-c"), and default_charging_enabled defaults to
false.
By default charging fails open (on_charging_error: :allow), matching the
previous HTTP charging path which logged failures but never blocked delivery:
- If a route requests charging but the Diameter stack is not enabled/ configured, the message is allowed (logged as a warning).
- If the OCS is unreachable or returns a protocol error, the message is allowed.
Set on_charging_error: :reject to fail closed instead — OCS errors and an
unconfigured stack are then treated as out of balance and the
out_of_balance_action applies.
Either way, a definitive out-of-balance answer from the OCS always triggers
the configured out_of_balance_action. Rejected messages are marked with
status :balance_rejected.
Diameter Sh (HSS) Configuration
OmniMessage can query the HSS via the Diameter Sh interface to detect on-net subscribers who are temporarily offline, preventing messages from being routed out the default gateway. See the HSS Subscriber Lookup Guide for full operational documentation.
# Enable HSS dip in the routing path
config :sms_c,
diameter_enabled: true
# Diameter stack configuration
config :diameter_ex,
diameter: %{
service_name: :omnimessage,
listen_ip: "0.0.0.0",
listen_port: 3868,
decode_format: :map,
host: "smsc01",
realm: "epc.mnc005.mcc547.3gppnetwork.org",
product_name: "OmniMessage",
request_timeout: 5000,
control_module: SmsC.Diameter.Control,
processor_module: DiameterEx.Processor,
vendor_id: 10415,
supported_vendor_ids: [10415],
applications: [
%{
application_name: :sh,
application_dictionary: :diameter_gen_3gpp_sh,
vendor_specific_application_ids: [
%{vendor_id: 10415, auth_application_id: 16_777_217, acct_application_id: nil}
]
}
],
peers: [
%{
host: "dra01.epc.mnc005.mcc547.3gppnetwork.org",
ip: "10.0.0.1",
port: 3868,
realm: "epc.mnc005.mcc547.3gppnetwork.org",
tls: false,
transport: :diameter_tcp,
initiate_connection: true
}
]
}
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
diameter_enabled | Boolean | No | false | Enable or disable HSS subscriber lookup in the routing path |
mock_sh | Boolean | No | false | Use mock HSS responses for testing (no Diameter stack started) |
mock_sh_on_net_numbers | List | No | [] | MSISDNs treated as on-net in mock mode |
For the full Diameter stack parameter reference (host, realm, peers, applications), see the HSS Subscriber Lookup Guide.
ENUM Configuration
DNS-based E.164 number lookups for intelligent routing.
Disable ENUM (Default)
# config/runtime.exs
config :sms_c,
enum_enabled: false
Enable ENUM with Default DNS
config :sms_c,
enum_enabled: true,
enum_domains: ["e164.arpa", "e164.org"],
enum_dns_servers: [], # Use system default DNS
enum_timeout: 5000 # 5 seconds
Enable ENUM with Custom DNS Servers
config :sms_c,
enum_enabled: true,
enum_domains: ["e164.internal.example.com", "e164.arpa"],
enum_dns_servers: [
{"10.0.1.53", 53}, # Internal DNS server
{"8.8.8.8", 53}, # Google Public DNS (fallback)
{"1.1.1.1", 53} # Cloudflare DNS (fallback)
],
enum_timeout: 3000 # 3 seconds (faster failover)
ENUM Domain Priority
Domains are queried in order until a successful lookup:
config :sms_c,
enum_domains: [
"e164.internal.example.com", # Try internal first
"e164.carrier.net", # Then carrier
"e164.arpa" # Then public registry
]
ENUM Performance Tuning
For Low-Latency Networks:
enum_timeout: 2000 # 2 seconds
For High-Latency/Satellite Links:
enum_timeout: 10000 # 10 seconds
ENUM DNS Setup Example
Configure Private ENUM Zone (BIND9 format):
; Zone file for e164.internal.example.com
$ORIGIN e164.internal.example.com.
$TTL 300
; Number: +1-555-0100 becomes 0.0.1.0.5.5.5.1.e164.internal.example.com
0.0.1.0.5.5.5.1.e164.internal.example.com. IN NAPTR 100 10 "u" "E2U+sip" "!^.*$!sip:15550100@voip-gateway.example.com!" .
0.0.1.0.5.5.5.1.e164.internal.example.com. IN NAPTR 100 20 "u" "E2U+pstn" "!^.*$!pstn:gateway-a.example.com!" .
; Number: +1-555-0200
0.0.2.0.5.5.5.1.e164.internal.example.com. IN NAPTR 100 10 "u" "E2U+sip" "!^.*$!sip:15550200@voip-gateway.example.com!" .
Test ENUM Resolution:
# Query ENUM domain
dig @10.0.1.53 NAPTR 0.0.1.0.5.5.5.1.e164.internal.example.com
# Expected output includes NAPTR records:
# 0.0.1.0.5.5.5.1.e164.internal.example.com. 300 IN NAPTR 100 10 "u" "E2U+sip" "!^.*$!sip:15550100@voip-gateway.example.com!" .
Number Translation Configuration
Regex-based number normalization applied before routing.
Disable Number Translation
# config/runtime.exs
config :sms_c,
translation_rules: []
Basic Number Translation Examples
Add Country Code to Local Numbers:
config :sms_c,
translation_rules: [
%{
calling_prefix: nil,
called_prefix: "",
source_smsc: nil,
calling_match: "^(\d{10})$", # Match 10-digit numbers
calling_replace: "+1\1", # Prepend +1
called_match: "^(\d{10})$",
called_replace: "+1\1",
priority: 100,
description: "Add +1 to 10-digit North American numbers",
enabled: true
}
]
Normalize International Format:
%{
calling_prefix: nil,
called_prefix: nil,
source_smsc: nil,
calling_match: "^00(\d+)$", # Match 00 prefix
calling_replace: "+\1", # Replace with +
called_match: "^00(\d+)$",
called_replace: "+\1",
priority: 10,
description: "Convert 00 international prefix to +",
enabled: true
}
Remove Formatting Characters:
%{
calling_prefix: nil,
called_prefix: nil,
source_smsc: nil,
calling_match: "^\+?1?[\s\-\.\(\)]*(\d{3})[\s\-\.\)\(]*(\d{3})[\s\-\.\(\)]*(\d{4})$",
calling_replace: "+1\1\2\3",
called_match: "^\+?1?[\s\-\.\(\)]*(\d{3})[\s\-\.\)\(]*(\d{3})[\s\-\.\(\)]*(\d{4})$",
called_replace: "+1\1\2\3",
priority: 50,
description: "Normalize US phone number formatting",
enabled: true
}
Carrier-Specific Translation
Route Code Stripping:
%{
calling_prefix: nil,
called_prefix: "101", # Only for 101 prefix
source_smsc: "carrier_a", # Only from this carrier
calling_match: nil, # Don't change calling
calling_replace: nil,
called_match: "^101(\d+)$", # Strip 101 route code
called_replace: "\1",
priority: 5,
description: "Strip carrier route code from called number",
enabled: true
}
Multi-Rule Translation
Rules are evaluated in priority order (lower number = higher priority):
config :sms_c,
translation_rules: [
# Priority 1: Most specific rules first
%{
calling_prefix: "1555",
called_prefix: nil,
source_smsc: nil,
calling_match: "^(1555\d{7})$",
calling_replace: "+\1",
called_match: nil,
called_replace: nil,
priority: 1,
description: "Premium number normalization",
enabled: true
},
# Priority 50: General rules
%{
calling_prefix: nil,
called_prefix: nil,
source_smsc: nil,
calling_match: "^(\d{10})$",
calling_replace: "+1\1",
called_match: "^(\d{10})$",
called_replace: "+1\1",
priority: 50,
description: "General 10-digit normalization",
enabled: true
}
]
Routing Configuration
Initial routing rules loaded on first startup. See SMS Routing Guide for complete routing documentation.
Load Routes from Configuration
# config/runtime.exs
config :sms_c,
sms_routes: [
# Geographic routing example
%{
calling_regex: nil,
called_regex: ~r/^\+1/,
source_smsc: nil,
dest_smsc: "north_america_gateway",
source_type: nil,
enum_domain: nil,
auto_reply: false,
auto_reply_message: nil,
drop: false,
charged: :default,
on_net_only: false,
weight: 100,
priority: 50,
description: "North America routing",
enabled: true
},
# Load balanced routing example
%{
calling_regex: nil,
called_regex: ~r/^\+44/,
source_smsc: nil,
dest_smsc: "uk_gateway_1",
source_type: nil,
enum_domain: nil,
auto_reply: false,
auto_reply_message: nil,
drop: false,
charged: :default,
on_net_only: false,
weight: 70,
priority: 50,
description: "UK primary gateway (70%)",
enabled: true
},
%{
calling_regex: nil,
called_regex: ~r/^\+44/,
source_smsc: nil,
dest_smsc: "uk_gateway_2",
source_type: nil,
enum_domain: nil,
auto_reply: false,
auto_reply_message: nil,
drop: false,
charged: :default,
on_net_only: false,
weight: 30,
priority: 50,
description: "UK backup gateway (30%)",
enabled: true
},
# On-net only — restrict an SMPP bind to on-net destinations only
%{
calling_regex: nil,
called_regex: nil,
source_smsc: "carrier_smpp_bind",
dest_smsc: "local_msc",
source_type: :smpp,
enum_domain: nil,
auto_reply: false,
auto_reply_message: nil,
drop: false,
charged: :default,
on_net_only: true,
weight: 100,
priority: 50,
description: "Carrier X — on-net termination only",
enabled: true
}
]
Skip Initial Route Loading
# Don't load routes from config (manage via Web UI only)
config :sms_c,
sms_routes: []
Routes defined in configuration are ONLY loaded if the routing table is empty (first startup).
Performance Tuning Configuration
See Performance Tuning Guide for detailed optimization strategies.
Batch Insert Worker
# config/config.exs
config :sms_c,
batch_insert_batch_size: 100, # Messages per batch
batch_insert_flush_interval_ms: 100 # Max wait time in ms
Performance Profiles:
| Profile | Batch Size | Interval | Throughput | Latency |
|---|---|---|---|---|
| High Volume | 200 | 200ms | ~5,000 msg/sec | Up to 200ms |
| Balanced | 100 | 100ms | ~4,500 msg/sec | Up to 100ms |
| Low Latency | 50 | 20ms | ~3,000 msg/sec | Up to 20ms |
| Real-time | 10 | 10ms | ~1,500 msg/sec | Up to 10ms |
Logging Configuration
Log Levels
# config/config.exs
config :logger, :console,
level: :info, # :debug, :info, :warning, :error
format: "$time $metadata[$level] $message\n",
metadata: [:request_id, :message_id, :route_id]
Production Recommended: :info or :warning
Development Recommended: :debug
Log Output Destinations
Console Only (Development):
config :logger,
backends: [:console]
File Logger (Production):
config :logger,
backends: [:console, {LoggerFileBackend, :file_log}]
config :logger, :file_log,
path: "/var/log/sms_c/application.log",
level: :info,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id, :message_id]
Log Rotation
Using logrotate (Linux):
# /etc/logrotate.d/sms_c
/var/log/sms_c/*.log {
daily
rotate 30
compress
delaycompress
notifempty
create 0644 sms_user sms_group
sharedscripts
postrotate
# Signal application to reopen log file
systemctl reload sms_c
endscript
}
Common Configuration Scenarios
High-Volume Aggregator
Optimize for maximum throughput (5,000+ messages/second):
# Database
config :sms_c, SmsC.Repo,
pool_size: 50
# Batch worker
config :sms_c,
batch_insert_batch_size: 200,
batch_insert_flush_interval_ms: 200
# Message retention
config :sms_c,
dead_letter_time_minutes: 1440 # 24 hours
# Charging (disabled for performance)
config :sms_c,
default_charging_enabled: false
# Cleanup (extended intervals)
config :sms_c,
cleanup_interval_minutes: 30
Enterprise Real-Time Messaging
Optimize for low latency (< 20ms):
# Database
config :sms_c, SmsC.Repo,
pool_size: 20
# Batch worker (low latency)
config :sms_c,
batch_insert_batch_size: 20,
batch_insert_flush_interval_ms: 10
# Message retention
config :sms_c,
dead_letter_time_minutes: 4320 # 3 days
# Charging (enabled) — balance checks over Diameter Ro
# (see the Charging Configuration section for the :charging + :diameter_ex blocks)
config :sms_c,
default_charging_enabled: true
Multi-Tenant Service Provider
Separate configuration per tenant:
# Tenant 1 environment
export DB_NAME=sms_c_tenant1
export NODE_NAME=sms_tenant1@node1.example.com
# Per-tenant OCS rating is selected via the :charging rating_profile config
# Tenant 2 environment
export DB_NAME=sms_c_tenant2
export NODE_NAME=sms_tenant2@node1.example.com
Geographic Redundancy
Cluster across regions:
# US East cluster
config :sms_c,
cluster_nodes: [
:"sms@us-east-1a.example.com",
:"sms@us-east-1b.example.com",
:"sms@us-west-1a.example.com" # Cross-region for DR
],
smsc_node_name: "us-east-1a"
Configuration Validation
Test configuration before deployment:
# Check configuration syntax
mix compile
# Validate database connection
mix ecto.create
mix ecto.migrate
# Check the Diameter Ro peer to the OCS (if charging enabled), in a remote console:
# :diameter.service_info(:omnimessage, :connections)
# Start application in interactive mode
iex -S mix phx.server
Environment Variables Reference
Common environment variables used in configuration:
| Variable | Purpose | Example |
|---|---|---|
DB_USERNAME | Database username | sms_prod_user |
DB_PASSWORD | Database password | strong_password |
DB_HOSTNAME | Database host | db.internal.example.com |
DB_PORT | Database port | 3306 |
DB_NAME | Database name | sms_c_production |
DB_POOL_SIZE | Connection pool size | 30 |
API_PORT | API listen port | 8443 |
API_LISTEN_IP | API listen IP | 0.0.0.0 |
WEB_PORT | Web UI port | 443 |
NODE_NAME | Erlang node name | sms@node1.example.com |
ERLANG_COOKIE | Cluster secret | shared_cookie_value |
Configuration Best Practices
- Use Environment Variables for sensitive values (passwords, API keys)
- Test Configuration Changes in staging before production
- Document Custom Settings in deployment notes
- Version Control Config Files (excluding secrets)
- Monitor After Changes for performance regressions
- Keep Backups of working configurations
- Validate Before Restart to avoid startup failures
- Use Consistent Naming across environments
- Set Resource Limits appropriate to hardware
- Review Periodically to remove unused features
Troubleshooting Configuration Issues
| Symptom | Likely Cause | Solution |
|---|---|---|
| Application won't start | Syntax error in config | Check logs, validate syntax |
| Database connection fails | Wrong credentials/host | Verify DB_* environment variables |
| API not accessible | Wrong port/IP binding | Check API_PORT and listen_ip |
| Cluster nodes won't connect | Cookie mismatch, firewall | Verify ERLANG_COOKIE, check ports 4369, 9100-9200 |
| Charging failures | OCS/DRA unreachable | Check Diameter Ro peer (charging fails open) |
| ENUM lookups fail | DNS server unreachable | Test DNS connectivity, check timeout |
| Poor performance | Wrong batch settings | Review Performance Tuning Guide |
| Messages not routing | Routes not loaded | Check sms_routes config or Web UI |
For additional help, see the Troubleshooting Guide.
Message Storage Configuration (Mnesia)
Message Retention
Messages are stored in Mnesia for fast access with configurable automatic cleanup.
config :sms_c,
# How long to keep messages in Mnesia (hours)
message_retention_hours: 24,
# How often to check for old messages (minutes)
retention_check_interval_minutes: 60
Recommendations:
- Production: 24-72 hours (balance operational needs vs memory)
- Development: 4-8 hours (faster cleanup for testing)
- High volume: 12-24 hours (conserve memory)
Memory Impact:
- Average message: ~1KB
- 10,000 messages: ~10MB
- 100,000 messages: ~100MB
CDR (Call Detail Record) Export
When messages are delivered or expired, CDRs can be automatically written to your Ecto database for long-term storage and billing analytics.
config :sms_c,
# Enable/disable CDR writing
cdr_enabled: true
CDR Records Include:
- Message ID, calling/called numbers
- Source/destination SMSC
- Origin/destination node (for clusters)
- Submission, delivery, expiry timestamps
- Status, delivery attempts
- Optional message body (see privacy controls)
When to Disable:
- Testing environments where CDRs aren't needed
- Temporary troubleshooting to reduce database load
Privacy Controls
Configure message body visibility and retention for privacy compliance.
config :sms_c,
# Delete message body from Mnesia after successful delivery
delete_message_body_after_delivery: false,
# Hide message body in web UI
hide_message_body_in_ui: false,
# Hide message body in CSV exports
hide_message_body_in_export: false
Use Cases:
| Configuration | Use Case |
|---|---|
delete_message_body_after_delivery: true | Save Mnesia space, privacy compliance |
hide_message_body_in_ui: true | Prevent operator viewing of message content |
hide_message_body_in_export: true | Data export compliance, sanitized reports |
Example Configurations:
Maximum Privacy (Compliance)
config :sms_c,
delete_message_body_after_delivery: true,
hide_message_body_in_ui: true,
hide_message_body_in_export: true,
cdr_enabled: true # Keep CDRs without bodies
Development (Full Visibility)
config :sms_c,
delete_message_body_after_delivery: false,
hide_message_body_in_ui: false,
hide_message_body_in_export: false,
cdr_enabled: true
Startup Logging
On application startup, configuration status is logged:
[info] Message storage: Mnesia (retention: 24h)
[info] CDR export: ENABLED
[info] Body deletion after delivery: DISABLED
[info] OCS charging: ENABLED (url: http://..., tenant: ...)
This provides immediate visibility into active features.