Skip to main content

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

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:

DatabaseVersionAdapterDefault PortBest For
MySQL8.0+Ecto.Adapters.MyXQL3306General purpose, proven reliability
MariaDB10.5+Ecto.Adapters.MyXQL3306MySQL-compatible, open source
PostgreSQL13+Ecto.Adapters.Postgres5432Advanced 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

WorkloadPool SizeDescription
Development5-10Minimal concurrency
Low Volume (< 100 msg/sec)10-15Small deployments
Medium Volume (100-1000 msg/sec)20-30Typical production
High Volume ( > 1000 msg/sec)40-100High-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 connection
  • ecto_pools_query_time - Query execution time
  • ecto_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"

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

ParameterTypeDefaultDescription
enabledBooleanfalseMaster switch for federation.
dns_srv_domainString""DNS SRV domain for peer discovery.
dns_poll_interval_msInteger30000DNS re-resolution interval.
health_check_interval_msInteger10000Peer health check interval.
registry_sync_interval_msInteger15000Frontend registry exchange interval.
forward_retry_interval_msInteger5000Queued message retry interval.
forward_max_retriesInteger50Max forward attempts before marking failed.
http_timeout_msInteger5000Timeout for all peer HTTP calls.
api_portInteger8443API port for constructing peer URLs.
static_peersList[]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
AttemptDelay
12 minutes
24 minutes
38 minutes
416 minutes
532 minutes
664 minutes
7128 minutes
8256 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:

chargedBehaviour
:yesAlways balance-check via the OCS
:noNever charge
:defaultUse 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
}
]
}
ParameterTypeRequiredDefaultDescription
diameter_enabledBooleanNofalseEnable or disable HSS subscriber lookup in the routing path
mock_shBooleanNofalseUse mock HSS responses for testing (no Diameter stack started)
mock_sh_on_net_numbersListNo[]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:

ProfileBatch SizeIntervalThroughputLatency
High Volume200200ms~5,000 msg/secUp to 200ms
Balanced100100ms~4,500 msg/secUp to 100ms
Low Latency5020ms~3,000 msg/secUp to 20ms
Real-time1010ms~1,500 msg/secUp 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:

VariablePurposeExample
DB_USERNAMEDatabase usernamesms_prod_user
DB_PASSWORDDatabase passwordstrong_password
DB_HOSTNAMEDatabase hostdb.internal.example.com
DB_PORTDatabase port3306
DB_NAMEDatabase namesms_c_production
DB_POOL_SIZEConnection pool size30
API_PORTAPI listen port8443
API_LISTEN_IPAPI listen IP0.0.0.0
WEB_PORTWeb UI port443
NODE_NAMEErlang node namesms@node1.example.com
ERLANG_COOKIECluster secretshared_cookie_value

Configuration Best Practices

  1. Use Environment Variables for sensitive values (passwords, API keys)
  2. Test Configuration Changes in staging before production
  3. Document Custom Settings in deployment notes
  4. Version Control Config Files (excluding secrets)
  5. Monitor After Changes for performance regressions
  6. Keep Backups of working configurations
  7. Validate Before Restart to avoid startup failures
  8. Use Consistent Naming across environments
  9. Set Resource Limits appropriate to hardware
  10. Review Periodically to remove unused features

Troubleshooting Configuration Issues

SymptomLikely CauseSolution
Application won't startSyntax error in configCheck logs, validate syntax
Database connection failsWrong credentials/hostVerify DB_* environment variables
API not accessibleWrong port/IP bindingCheck API_PORT and listen_ip
Cluster nodes won't connectCookie mismatch, firewallVerify ERLANG_COOKIE, check ports 4369, 9100-9200
Charging failuresOCS/DRA unreachableCheck Diameter Ro peer (charging fails open)
ENUM lookups failDNS server unreachableTest DNS connectivity, check timeout
Poor performanceWrong batch settingsReview Performance Tuning Guide
Messages not routingRoutes not loadedCheck 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:

ConfigurationUse Case
delete_message_body_after_delivery: trueSave Mnesia space, privacy compliance
hide_message_body_in_ui: truePrevent operator viewing of message content
hide_message_body_in_export: trueData 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.