A Worker module for Apostol + db-platform — Apostol CRM1.
WebSocket API provides real-time WebSocket connectivity to the API. It implements a lightweight JSON-based RPC protocol over WebSocket, an event subscription system (Observer pattern), and a REST endpoint for pushing data to connected clients.
WebSocketAPI is tightly coupled to the observer, notification, notice, message, and log modules of db-platform.
The C++ module handles WebSocket transport and session routing. All subscription state, publisher registration, and event data live entirely in the database:
| db-platform module | Purpose |
|---|---|
observer |
Publisher/subscriber registry: stores publishers (observer.publisher) and active listener subscriptions (observer.listener) per session |
notification |
Source of notify publisher events — fires whenever a user interacts with a system object (create/update/delete/transition) |
notice |
Source of notice publisher events — system-level notices grouped by category |
message |
Source of message publisher events — inbox and outbox message records from the message entity |
log |
Source of log publisher events — event log entries (M/W/E/D) from the system log |
Key database objects:
| Object | Purpose |
|---|---|
observer.publisher |
Registered publishers (notify, notice, message, log, geo) |
observer.listener |
Active subscriptions: session → publisher with filter and params |
api.observer_subscribe(publisher, filter, params) |
Creates or updates a listener for the current session |
api.observer_unsubscribe(publisher) |
Removes the listener for the current session |
api.observer_publisher(code) |
Returns publisher metadata |
api.observer_listener(publisher, session) |
Returns listener state for a session |
[module/WebSocketAPI]
enable=trueFollow the build and installation instructions for Apostol.
To establish a WebSocket connection the client must perform an Opening Handshake as described in RFC 6455, Section 4.
The server imposes additional constraints on the WebSocket URL, described below.
The WebSocket connection is scoped to a previously created session. A session is created after successful user authentication, which yields an access token, a session code, and a secret key.
The connection URL contains the session code and an optional identity that distinguishes multiple connections within the same session.
URL format:
ws[s]://[ws.]example.com/session/<code>[/<identity>]
Where:
<code>— Required. Session code (40 characters).<identity>— Optional. Connection identity within the session. Used to maintain multiple simultaneous connections to the same session.
Examples:
wss://ws.example.com/session/c83b2f85321f95341707624546ca6ac4fa6d1115
wss://ws.example.com/session/c83b2f85321f95341707624546ca6ac4fa6d1115/user1
The WebSocket protocol itself does not provide request/response semantics. To enable this, a small JSON-based RPC protocol is layered on top of WebSocket.
| Key | Name | Type | Description |
|---|---|---|---|
t |
MessageTypeId | INTEGER | Message type (see below). |
u |
UniqueId | UUID | Unique message identifier. When a server message is a reply to a client request, both share the same UniqueId. |
a |
Action | STRING | API endpoint route. |
c |
ErrorCode | INTEGER | Error code. |
m |
ErrorMessage | STRING | Error description. |
p |
Payload | JSON | Message payload. |
| Type | Number | Direction | Description |
|---|---|---|---|
OPEN |
0 | Client → Server | Authorize an existing session. |
CLOSE |
1 | Client → Server | Close the session (sign out). |
CALL |
2 | Client ↔ Server | Request or server-initiated push. |
CALLRESULT |
3 | Server → Client | Successful response to a CALL. |
CALLERROR |
4 | Server → Client | Error response to a CALL. |
After connecting, the client must authorize before sending API requests.
Authorization can be performed automatically during the handshake if the following HTTP headers are provided:
Authorization: Bearer <token>
Or:
Session: <session>
Secret: <secret>
If the client framework prevents setting custom HTTP headers during the WebSocket handshake, authorization is performed by sending an OPEN message with credentials issued by the AuthServer. Either an access token (token) or a session secret (secret) may be used.
After successful authorization, CALL messages can be sent. The API endpoint is specified in the Action field (a) and the JSON request body in the Payload field (p).
Attempting to send a CALL before authorization results in a CALLERROR response.
Example — session code not found or session closed:
{"t":4,"u":"<uuid>","c":400,"m":"Session code not found."}Request:
{"t":0,"u":"<uuid>","p":{"secret": "MWCJ14k/RJyiHskQB8DoVbliiwDeNGKsgsAMugp3OZt+M0Zj44hDykwRuFoWEwuG"}}Success response:
{"t":3,"u":"<uuid>","p":{"authorized": true, "code": "amAJmzkxvDE+ad7KwkRtZU1qkUod+3XuycBbxRqHOOjBdeOkkR+lSExI4L8LAcb+", "message": "Success."}}Where code is a new authorization code for obtaining an access token (not to be confused with the session secret).
Error response:
{"t":4,"u":"<uuid>","c":401,"m":"Sign out. Session secret failed verification."}Note: If incorrect authorization credentials are supplied, the session is closed but the WebSocket connection remains open.
Request:
{"t":0,"u":"<uuid>","p":{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiIDogImFjY291bnRzLnBsdWdtZS5ydSIsICJhdWQiIDogInNlcnZpY2UtcGx1Z21lLnJ1IiwgInN1YiIgOiAiYzgzYjJmODUzMjFmOTUzNDE3MDc2MjQ1NDZjYTZhYzRmYTZkMTExNSIsICJpYXQiIDogMTYwNjIxMDcwNiwgImV4cCIgOiAxNjA2MjE0MzA2fQ.ZI82FKXAgA1CZm3gx9XCpgpq_WyZJvwqYI4nOdccVts"}}Note: Access tokens have a limited lifetime.
Success response:
{"t":3,"u":"<uuid>","p":{"authorized": true, "message": "Success."}}Error response:
{"t":4,"u":"<uuid>","c":403,"m":"Verification failed: Token expired."}Arbitrary data can be pushed to a connected WebSocket client via a REST API call:
POST /ws/<code>[/<identity>]
<anydata>
Where:
<code>— Required. Session code of the target WebSocket connection.<identity>— Optional. Connection identity within the session (if applicable).<anydata>— Optional. Any data in any format.
The data is delivered to the client as a CALL message with Action set to /ws and Payload containing the body of the REST request.
Example request:
POST /ws/8c98085f34c83a0eea5f40791218fbf80f1858d3 HTTP/1.1
Host: localhost:8080
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.[...].9GI82ffkIhbUeWR8if3a8q78nfXAL4AFOMp3kWDTHOA
Content-Type: application/json
{"anydata":null}Success response:
{"sent": true, "status": "Success"}Error response:
{"sent": false, "status": "Session not found"}To receive server-initiated data without polling, a client subscribes to events from a publisher.
To subscribe, choose a publisher and configure a listener (filter and parameters).
The notify publisher delivers system events that are raised whenever a user interacts with an object in the system.
{
"entities": ["<code>", ...],
"classes": ["<code>", ...],
"actions": ["<code>", ...],
"methods": ["<code>", ...],
"objects": [<id>, ...]
}| Field | Type | Description |
|---|---|---|
entities |
JSON array of strings | Optional. Entity codes. |
classes |
JSON array of strings | Optional. Class codes. |
actions |
JSON array of strings | Optional. Action codes. |
methods |
JSON array of strings | Optional. Method codes. |
objects |
JSON array of integers | Optional. Object IDs. |
Important: Fields are combined with AND logic; multiple values within a field use OR logic. Fields with no values specified are ignored.
{
"type": "notify | object | mixed | hook",
"hook": { ... }
}| Field | Type | Values | Description |
|---|---|---|---|
type |
STRING | notify, object, mixed, hook |
Optional. Response type. |
hook |
JSON | Hook object | Required when type is hook. |
notify— deliver the raw notification.object— deliver the object data as returned by a/getrequest.mixed— deliver both the notification and the object data.hook— deliver the result of the API request defined inhook.
A hook defines an API request to execute each time the subscription condition is met.
{
"method": "POST | GET",
"path": "<api-path>",
"payload": { ... }
}| Field | Type | Description |
|---|---|---|
method |
STRING | Optional. HTTP method (POST or GET). |
path |
STRING | Required. REST API path. |
payload |
JSON | Optional. Request payload. Depends on the endpoint. |
The notice publisher delivers system notices.
{
"categories": ["<code>", ...]
}| Field | Type | Description |
|---|---|---|
categories |
JSON array of strings | Optional. Category codes. |
{
"type": "notify"
}| Field | Type | Description |
|---|---|---|
type |
STRING | Optional. Response type. Currently notify. |
The message publisher delivers inbound and outbound messages.
{
"classes": ["inbox", "outbox"],
"types": ["<code>", ...],
"agents": ["<code>", ...],
"codes": ["<code>", ...],
"profiles": ["<value>", ...],
"addresses": ["<value>", ...],
"subjects": ["<value>", ...]
}| Field | Type | Description |
|---|---|---|
classes |
JSON array | Optional. Message direction: inbox or outbox. |
types |
JSON array of strings | Optional. Agent type codes. |
agents |
JSON array of strings | Optional. Agent codes. |
codes |
JSON array of strings | Optional. Message codes. |
profiles |
JSON array | Optional. Settings profile or sender address. |
addresses |
JSON array | Optional. Recipient address (for API requests — the REST route). |
subjects |
JSON array | Optional. Message subject. |
{
"type": "notify"
}| Field | Type | Description |
|---|---|---|
type |
STRING | Optional. Response type. Currently notify. |
The log publisher delivers event log entries.
{
"types": ["M", "W", "E", "D"],
"codes": [<integer>, ...],
"categories": ["<code>", ...]
}| Field | Type | Description |
|---|---|---|
types |
JSON array of strings | Optional. Log level: M (Message), W (Warning), E (Error), D (Debug). |
codes |
JSON array of integers | Optional. Numeric log codes. |
categories |
JSON array of strings | Optional. Category codes. |
{
"type": "notify"
}| Field | Type | Description |
|---|---|---|
type |
STRING | Optional. Response type. Currently notify. |
The geo publisher delivers incoming geolocation data.
{
"codes": ["<code>", ...],
"objects": [<id>, ...]
}| Field | Type | Description |
|---|---|---|
codes |
JSON array of strings | Optional. Coordinate group codes (locations). Defaults to default. |
objects |
JSON array of integers | Optional. Object IDs. |
{
"type": "notify"
}| Field | Type | Description |
|---|---|---|
type |
STRING | Optional. Response type. Currently notify. |
POST /api/v1/observer/subscribe
Subscribe to a publisher's events.
Request fields:
| Field | Type | Values | Description |
|---|---|---|---|
publisher |
STRING | notify, notice, message, log, geo |
Required. Publisher code. |
filter |
JSON | Optional. Event filter. | |
params |
JSON | Optional. Listener parameters. |
Examples:
Subscribe to all events from the notify publisher:
{"t":2,"u":"<uuid>","a":"/observer/subscribe","p":{"publisher":"notify"}}Subscribe with a filter (classes: client, device) and response type object:
{"t":2,"u":"<uuid>","a":"/observer/subscribe","p":{"publisher":"notify","filter":{"classes":["client","device"]},"params":{"type":"object"}}}Subscribe to all incoming messages:
{"t":2,"u":"observer","a":"/observer/subscribe","p":{"publisher":"notify","filter":{"entities":["message"],"classes":["inbox"],"actions":["create"]},"params":{"type":"object"}}}Catch new client creation and receive the result as a client list:
{"t":2,"u":"<uuid>","a":"/observer/subscribe","p":{"publisher":"notify","filter":{"classes":["client"],"actions":["create"]},"params":{"type":"hook","hook":{"path":"/api/v1/client/list","payload":{}}}}}POST /api/v1/observer/unsubscribe
Unsubscribe from a publisher's events.
Request fields:
| Field | Type | Values | Description |
|---|---|---|---|
publisher |
STRING | notify, notice, message, log, geo |
Required. Publisher code. |
Example:
{"t":2,"u":"<uuid>","a":"/observer/unsubscribe","p":{"publisher":"notify"}}POST /api/v1/observer/publisher
Request fields:
| Field | Type | Values | Description |
|---|---|---|---|
code |
STRING | notify, notice, message, log, geo |
Required. Publisher code. |
fields |
JSON array | Optional. Array of field names to return. If omitted, all fields are returned. |
POST /api/v1/observer/publisher/get
Request fields:
| Field | Type | Values | Description |
|---|---|---|---|
code |
STRING | notify, notice, message, log, geo |
Required. Publisher code. |
fields |
JSON array | Optional. Array of field names to return. If omitted, all fields are returned. |
POST /api/v1/observer/publisher/count
Request fields: Common list query parameters
POST /api/v1/observer/publisher/list
Request fields: Common list query parameters
POST /api/v1/observer/listener
Request fields:
| Field | Type | Values | Description |
|---|---|---|---|
publisher |
STRING | notify, notice, message, log, geo |
Required. Publisher code. |
session |
STRING | Optional. Session code. | |
fields |
JSON array | Optional. Array of field names to return. If omitted, all fields are returned. |
POST /api/v1/observer/listener/set
Request fields:
| Field | Type | Description |
|---|---|---|
publisher |
STRING | Required. Publisher identifier. |
session |
STRING | Optional. Session code. |
filter |
JSON | Optional. Event filter. |
params |
JSON | Optional. Listener parameters. |
POST /api/v1/observer/listener/get
Request fields:
| Field | Type | Description |
|---|---|---|
publisher |
STRING | Required. Publisher code. |
session |
STRING | Optional. Session code. |
fields |
JSON array | Optional. Array of field names to return. If omitted, all fields are returned. |
POST /api/v1/observer/listener/count
Request fields: Common list query parameters
POST /api/v1/observer/listener/list
Request fields: Common list query parameters
Who am I:
{"t":2,"u":"<uuid>","a":"/whoami"}Query entities:
{"t":2,"u":"<uuid>","a":"/entity","p":{"fields":["id","code","name"]}}Query classes:
{"t":2,"u":"<uuid>","a":"/class","p":{"fields":["id","entity","entitycode","entityname","code","label"]}}Query actions:
{"t":2,"u":"<uuid>","a":"/action","p":{"fields":["id","code","name"]}}Query methods:
{"t":2,"u":"<uuid>","a":"/method","p":{"fields":["id","class","classcode","classlabel","action","actioncode","actionname","code","label"]}}Footnotes
-
Apostol CRM is an abstract term, not a standalone product. It refers to any project that uses both the Apostol C++ framework and db-platform together through purpose-built modules and processes. Each framework can be used independently; combined, they form a full-stack backend platform. ↩