Skip to content

Platform Modules

A module is a separate service that extends the platform without being linked into the trading core. Modules own their storage, their release cycle, and their provider integrations; the core owns trading, accounts, and market data.

Everything a module publishes becomes a regular platform method. From the outside there is no difference between a method served by the C++ core and a method served by a module: the same authorization, the same transports, the same request and response shape.

Available modules

Module Prefix Purpose
Click2Call Click2Call* Outbound calls to customers and leads through telephony providers, call statuses, recordings, DNC list
SMS Sms* Transactional and marketing SMS through telephony providers, queue and retries, templates and triggers, delivery reports, DNC and spend reporting
Mailer Mailer* Transactional and marketing email, templates and campaigns, suppression list and unsubscribe, deliverability reporting and sender-domain verification

How a module is connected

A module connects to the platform through NATS and speaks the Moleculer protocol. It does not open a listening port for clients and is never addressed directly.

flowchart LR
    web["Web terminal / Backoffice"] -->|HTTPS| router
    s2s["server-api-js / CRM backend"] -->|TCP| router
    router["Platform Router"] <-->|NATS| module["Module<br/>(Moleculer service)"]
    module --> db[("Module storage")]
    module --> provider["External provider<br/>(telephony, email, SMS)"]

On start the module registers its actions in the service registry and announces them with $node.info. The platform Router reads that announcement and, for every action that declares a route, registers two entry points:

Announced field Used as
name TCP/WS command name
route.method + route.path HTTP method and path

The Router forwards the call over NATS and returns the module response to the caller. If the module is down, the platform answers 502 MOL_CALL_FAILED — the request never blocks the trading core.

Method naming

A method name is the contract. It is published as-is, so it must be readable on its own, without knowing which process serves it.

  • Action name<Module><Verb><Entity>, PascalCase: Click2CallStartCall, SmsSendMessage, MailerSendEmail, MailerPing.
  • REST path<module>/<resource>[/<action>], lowercase module prefix: click2call/call/start, sms/message, mailer/email, mailer/emails/list.

The module prefix is part of the name, not of the transport address. Several modules share one bus and one HTTP host, so an unprefixed Ping or handleWebhooks/:uuid would be claimed by whichever module connected first. Names are published without the service prefix, so renaming or scaling a process never changes a public method name.

Transports

Every module method is available through both transports. Pick by integration type, not by feature — the parameters and the result are identical.

REST API TCP API
Addressing HTTP method + path command name
Typical caller web terminal, mobile app, backoffice, webhooks CRM backend, integrations, bots, server-api-js
Auth JWT in Authorization session token of the TCP connection
Shape request body / query, HTTP status code { command, extID, data } envelope
Streaming events no yes, module events arrive on the same connection

Each module documents both: Click2Call REST and TCP, SMS REST and TCP, Mailer REST and TCP.

Authorization

The platform authenticates the caller and passes the resolved identity to the module in __access:

{
  "__access": {
    "id": 10,
    "type": 2,
    "brand": "default",
    "roles": ["manager"]
  }
}

Modules never trust request fields for identity: brand and author are taken from __access, so a manager cannot read another brand's data by sending a different brand value. Access requirements of each method are listed on its page.

Response shape

The envelope belongs to the transport, not to the method. A module action returns the payload; the service protocol wraps it on the wire:

{ "id": "…", "meta": { }, "success": true, "data": { } }

A failed action returns the error instead, with the code the module assigned to it:

{
  "success": false,
  "error": {
    "code": 409,
    "type": "PHONE_BLACKLISTED",
    "message": "Phone 380950000000 is blacklisted: customer request"
  }
}

The platform reads that envelope and gives the caller only what it asked for: data with HTTP 200 on success, or error.code as the status and error.message as the body on failure. Method pages document the payload and the error codes — the envelope around them is always the same.

Because the envelope is added by the transport, a module must not put its own success field into a payload: it would arrive as {"success": true, "data": {"success": true, …}} and force clients to unwrap two different shapes depending on the method.

Module storage

A module keeps its own database. Nothing outside the module reads those tables directly: the state that other services need is published as events, and everything else is available through the module methods. That is what makes a module replaceable — and what keeps a module outage from turning into a platform outage.