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.

Modules and the platform can also exchange asynchronous messages through the module event bus. Events complement methods when the publisher does not need a synchronous response.

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

Event contracts

The event bus has two explicit directions:

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.