Skip to content

SMPP Stack

A Bun + Turborepo monorepo that delivers SMS through a carrier SMSC over SMPP, and builds an on-call paging product on top of it.

The stack splits into two layers.

  1. The transport layer: (Bun/Elysia API), services/rabbitmq (broker), and services/smpp (Rust SMPP transceiver) accepts message requests over HTTP, persists them, and submits them to the SMSC, tracking every status transition back to the database.
  2. The product layer ingests Huawei network alarms from an IMAP mailbox, resolves who is on call from an escalation ladder synced out of Microsoft Entra, and pages them by SMS (through the transport layer) and email.

Message Flow


The "status update" half of the return path is performed by the server app after it decodes the protobuf message.

sendToQueue protobuf Message basic_consume smpp_consumer submit_sm registered_delivery request_all deliver_sm / DLR status SENDING status SENT status FAILED plus error_reason apps/server in-process consumer queue netops-smpp StreamConsumer delegate MessageProcessor process_stream_entry deserialize_message prost decode mpsc send_tx capacity 100 run_listener connected loop lookup preflight_check when SMS_LOOKUP_API_KEY set do_send builds SubmitSm SMSC Event Incoming DeliverSm Incoming from_event Message status RECEIVED source smpp-server basic_publish exchange smpp rk to_nodejs queue to_nodejs apps/server setupReceiveChannel Postgres message, message_smpp, message_status_history WebSocket topic smpp

Session Lifecycle

health.smpp_bound is set true immediately after a successful bind and false on ConnectionLost.
The Idle state deliberately leaves it true, so an idle-unbound service still reports healthy.

TCP connect ok connect error bind_transceiver_resp ESME_ROK bind error, client.close first enquire_link every 20s, auto EnquireLinkResp submit_sm or deliver_sm resets inactivity timer no PDU for SMS_INACTIVITY_TIMEOUT_SECS, unbind + close outbound message arrives, stored as pending_message event stream ended, ConnectionLost sleep min 5s times 2^attempts, capped 60s shutdown signal, unbind + close shutdown signal Connecting Binding Backoff Bound Idle Stopped

SMPP Transceiver Service

Rust ESME for the stack. It binds to the carrier SMSC as an SMPP transceiver, consumes protobuf-encoded work items from RabbitMQ, submits them as submit_sm, and publishes every status transition plus every inbound deliver_sm back onto the exchange. Here the server app persists it to Postgres and fans it out over WebSocket. The transceiver service does not hold a database connection; it is a pure protocol bridge between AMQP and the SMSC with an optional pre-send number-type lookup and an HTTP readiness probe.

SMPP Server App

HTTP/API front door for SMPP messaging in the stack. A Bun + Elysia application that accepts message requests over HTTP, validates and persists them to Postgres, publishes work batches to RabbitMQ, hands protobuf-encoded messages to the Rust smpp transceiver for submission to the SMSC, consumes status updates coming back from that transceiver, maintains a full status history per message, and exposes monitoring, message-template CRUD, WebSocket fan-out, authentication (better-auth) and an OpenAPI document.

Outbound path

POST /api/sms/v1/smpp/send hasAuth macro (x-api-key or session) valibot MessageBodySchema insert message (scheduled=true) insert message_smpp insert message_status_history SCHEDULED 201 Created sendToQueue(AMQP_SMPP_QUEUE, JSON batch) deliver batch claim row (scheduled true to false) insert history PENDING sendToQueue(AMQP_QUEUE, protobuf Message) insert history SENT or FAILED deliver protobuf submit_sm (registered_delivery=all) Client Elysia route Postgres RabbitMQ In-process consumer Rust smpp service SMSC

Inbound / status path

opt [body starts with ack] alt [message.id unknown to Postgres] [known message] publish(smpp exchange, rk to_nodejs) deliver_sm (inbound SMS / receipt) publish Message status RECEIVED protobuf Message fromBinary(MessageSchema) forward JSON to AMQP_PAGING_QUEUE insert message + message_smpp enqueue if status SCHEDULED publish UPDATE_MESSAGE insert message_status_history ack SENDING before submit_sm SENT on success, FAILED with error_reason SMSC Rust smpp service RabbitMQ to_nodejs consumer Postgres WebSocket clients paging queue

Paging App

Product-level integration of the netops-paging stack. This Bun + Elysia app ingests network alarms from a Huawei alarm mailbox over IMAP, normalizes them into the paging Postgres database, resolves who is currently on call from Microsoft Entra group membership, and pages that person by SMS (through the RabbitMQ -> apps/server -> services/smpp chain) and/or email (direct SMTP). Inbound "ACK" replies flow back through the same broker and clear the alarms. Alarm ingestion, on-call resolution, and dispatch are three separate stages that communicate only through Postgres. The active_alarms view is the boundary: the mail side writes alarm_instance rows, the paging side reads the view (which has already resolved the next escalation level and its contact addresses) and emits messages.

fetchUnseenMessages, mark Seen, move to Trash Parse + Validate + insertAlarm processOncallMessages next_escalation + addresses insert paging_run rows protobuf Message to queue to_nodejs transport.sendMail JSON batch to AMQP_SMPP_QUEUE SMPP submit_sm group members, group info cached lists + bearer token staff / contact / staff_team / staff_social Huawei alarm mailbox (IMAP 993) MailService (src/modules/mail) Postgres — paging DB active_alarms view AlarmsService (src/modules/alarms) SMTP relay (nodemailer pool) RabbitMQ client-vhost apps/server (Bun/Elysia) services/smpp (Rust transceiver) On-call phones Microsoft Graph — Entra groups OncallService (src/modules/oncall) Redis / Valkey