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:
- Platform events for modules publish selected typed platform changes.
- Platform event listeners expose asynchronous platform capabilities.
- Message delivery events define the initial account and manager delivery API.
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.