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.