RPC Bridge
The RPC Bridge exposes Strata services over HTTP and WebSockets. It sits between external consumers (browsers, mobile apps, third-party systems) and your Strata backend, translating HTTP POST requests and Socket.IO messages into Strata service calls.
The bridge is split into two packages:
@strata-js/rpcbridge-- the server that accepts HTTP/WS requests and forwards them to Strata services.@strata-js/rpcbridge-client-- a lightweight browser/Node client for calling services through the bridge via WebSocket.
Installation
# Server
npm install @strata-js/rpcbridge
# Client (browser or Node)
npm install @strata-js/rpcbridge-clientServer
Standalone Usage
The simplest way to run the bridge is to let it create its own Express and Socket.IO server:
import { RPCBridgeServer } from '@strata-js/rpcbridge';
const config = {
server: {
enableHTTP: true,
enableWS: true,
port: 3000,
path: '/api',
excludeStack: true,
excludeUnsafeMessages: true,
},
strata: {
backend: { type: 'redis', redis: { host: 'localhost', port: 6379 } },
client: { name: 'RPCBridge' },
},
globalBlockList: [ 'service:*' ],
services: {
users: {
serviceGroup: 'UserService',
blockList: [ 'admin:*' ],
},
orders: {
serviceGroup: 'OrderService',
allowList: [ 'cart:*', 'checkout:*' ],
},
},
};
const bridge = new RPCBridgeServer(config);
bridge.startListening();Bring Your Own Server
If you already have an Express app and want to mount the bridge alongside your own routes:
import http from 'http';
import express from 'express';
import { Server as SIOServer } from 'socket.io';
import { RPCBridgeServer } from '@strata-js/rpcbridge';
const app = express();
const server = http.createServer(app);
const io = new SIOServer(server);
// Your own routes
app.get('/health', (_req, res) => { res.send('ok'); });
// Mount the bridge -- pass your server instances as the third argument
new RPCBridgeServer(config, undefined, {
expressApp: app,
httpServer: server,
ioServer: io,
});
// You manage the listener
server.listen(3000);When providing external server instances, calling startListening() is unnecessary -- you control the listener yourself.
Configuration
ServerConfig
The top-level config object passed to RPCBridgeServer:
interface ServerConfig
{
server : WebServerConfig;
strata : StrataClientConfig;
globalBlockList : string[];
services : ServiceConfig;
}WebServerConfig
interface WebServerConfig
{
enableHTTP : boolean; // Enable the HTTP POST endpoint
enableWS : boolean; // Enable the WebSocket endpoint
port : number; // Port to listen on (standalone mode)
path : string; // Base path for both endpoints (e.g. '/api')
excludeStack : boolean; // Strip stack traces from error responses
excludeUnsafeMessages : boolean; // Replace non-safe error messages with a generic string
allowServiceGroupOverride ?: boolean; // Allow callers to specify serviceGroup directly
}ServiceConfig
A map of service names to their configuration. The key is the serviceName callers use in requests:
type ServiceConfig = Record<string, ServiceEntry>;
interface ServiceEntry
{
serviceGroup : string; // The Strata service group to forward requests to
blockList ?: string[]; // Operations to deny (e.g. [ 'admin:*' ])
allowList ?: string[]; // Operations to allow (takes precedence over blockList)
}Block and Allow Lists
Each service entry can have a blockList or allowList. Entries follow the context:operation pattern. Use * as the operation to match all operations in a context.
There is also a globalBlockList on the top-level config that applies to every service.
Evaluation order:
globalBlockListis checked first. If the call matches, it is denied.- If the service has an
allowList, only calls matching the list are permitted. Everything else is denied. - If the service has a
blockList(and noallowList), calls matching the list are denied. Everything else is permitted. allowListtakes precedence -- if both are defined,blockListis ignored.
services: {
users: {
serviceGroup: 'UserService',
// Only these operations are reachable through the bridge
allowList: [ 'profile:get', 'profile:update' ],
},
admin: {
serviceGroup: 'AdminService',
// Everything is reachable except these
blockList: [ 'system:shutdown', 'system:reset' ],
},
},HTTP Endpoint
When enableHTTP is true, the bridge mounts a POST route at the configured path. Send a JSON body with the following shape:
{
"serviceName": "users",
"context": "profile",
"operation": "get",
"payload": { "userId": "abc123" },
"meta": {},
"auth": "Bearer ...",
"timeout": 5000
}| Field | Type | Required | Description |
|---|---|---|---|
serviceName | string | yes* | Key from the services config map. |
serviceGroup | string | yes* | Direct service group name (requires allowServiceGroupOverride). |
context | string | yes | Target context name. |
operation | string | yes | Target operation name. |
payload | object | yes | Request payload. |
meta | object | no | Metadata passed through to the service. |
auth | string | no | Authentication token. |
timeout | number | no | Request timeout in milliseconds. |
*Either serviceName or serviceGroup must be provided, not both.
On success the response body is the service's response payload. On failure the response is a 400 with an error object.
WebSocket Endpoint
When enableWS is true, the bridge listens on a Socket.IO namespace at the configured path. Clients emit an rpc event with the same request shape as the HTTP endpoint and receive the response via a Socket.IO acknowledgement callback.
The response is an RPCResponse object:
interface RPCResponse
{
status : 'success' | 'failed';
response : unknown;
}Middleware
You can inject middleware for both HTTP and WebSocket endpoints by passing a MiddlewareConfig as the second constructor argument:
import type { HTTPMiddleware, SocketMiddleware, MiddlewareConfig } from '@strata-js/rpcbridge';
const httpLogger : HTTPMiddleware = (req, _res, next) =>
{
console.log('HTTP request:', req.body);
next();
};
const wsAuth : SocketMiddleware = (_socket, rpcRequest, next) =>
{
rpcRequest.auth = 'injected-token';
next();
};
const middleware : MiddlewareConfig = {
http: [ httpLogger ],
socket: [ wsAuth ],
};
new RPCBridgeServer(config, middleware);HTTP middleware follows the standard Express signature (req, res, next). Middleware runs before the request is forwarded to Strata.
WebSocket middleware receives (socket, rpcRequest, next). Call next() to continue or next('error message') to reject the request.
type HTTPMiddleware = (req : Request, res : Response, next : () => void) => void;
type SocketMiddleware = (socket : Socket, rpcRequest : RPCRequest, next : (errMsg ?: string) => void) => void;
interface MiddlewareConfig
{
http ?: HTTPMiddleware[];
socket ?: SocketMiddleware[];
}Client
The client package provides a WebSocket-based client for calling Strata services through the RPC Bridge.
Basic Usage
import { WebSocketClient } from '@strata-js/rpcbridge-client';
const client = new WebSocketClient('http://localhost:3000/api');
const user = await client.request<User>(
'users', 'profile', 'get',
{ userId: 'abc123' }
);
console.log(user);Constructor
new WebSocketClient(uri : string, opts ?: ClientConstructorOptions)| Parameter | Type | Description |
|---|---|---|
uri | string | The bridge server URL including the path (e.g. http://localhost:3000/api). |
opts | ClientConstructorOptions | Socket.IO client options plus an optional emitTimeout. Defaults to { transports: [ 'websocket' ], reconnection: true, emitTimeout: 20000 }. |
request()
client.request<T>(
serviceName : string,
context : string,
operation : string,
payload : Record<string, unknown>,
meta ?: Record<string, unknown>,
auth ?: string,
timeout ?: number
) : Promise<T>Sends a request through the bridge and returns the service's response payload. Throws RemoteServiceError if the service returned a failure, or EmitTimeoutError if the request times out.
| Parameter | Type | Required | Description |
|---|---|---|---|
serviceName | string | yes | The service name as configured in the bridge's services map. |
context | string | yes | Target context. |
operation | string | yes | Target operation. |
payload | Record<string, unknown> | yes | Request payload. |
meta | Record<string, unknown> | no | Metadata. |
auth | string | no | Authentication token. |
timeout | number | no | Override the default emitTimeout. |
Error Handling
The client throws typed errors:
RemoteServiceError-- the bridge returned a'failed'status. Containscode,message,name,stack, and optionaldetailsfrom the remote service.EmitTimeoutError-- the Socket.IO emit timed out waiting for an acknowledgement.
import { RemoteServiceError, EmitTimeoutError } from '@strata-js/rpcbridge-client';
try
{
await client.request('users', 'profile', 'get', { userId: 'bad' });
}
catch(err)
{
if(err instanceof RemoteServiceError)
{
console.error(`Service error [${ err.code }]: ${ err.message }`);
}
else if(err instanceof EmitTimeoutError)
{
console.error('Request timed out');
}
}Accessing the Socket
If you need lower-level access to the Socket.IO client (for custom events, connection state, etc.):
const socket = client.socket;
socket.on('connect', () => { console.log('Connected'); });
socket.on('disconnect', () => { console.log('Disconnected'); });Examples
Full Stack Example
A minimal setup with a Strata service, the RPC Bridge, and a browser client calling through it:
Service (service.ts):
import { StrataService, Context } from '@strata-js/strata';
const service = new StrataService({
service: { serviceGroup: 'GreeterService' },
backend: { type: 'redis', redis: { host: 'localhost', port: 6379 } },
});
const greeter = new Context('greeter');
greeter.registerOperation('hello', async (request) =>
{
return { message: `Hello, ${ request.payload.name }!` };
});
service.registerContext(greeter);
await service.start();Bridge (bridge.ts):
import { RPCBridgeServer } from '@strata-js/rpcbridge';
const bridge = new RPCBridgeServer({
server: {
enableHTTP: true,
enableWS: true,
port: 3000,
path: '/rpc',
excludeStack: true,
excludeUnsafeMessages: false,
},
strata: {
backend: { type: 'redis', redis: { host: 'localhost', port: 6379 } },
client: { name: 'RPCBridge' },
},
globalBlockList: [],
services: {
greeter: { serviceGroup: 'GreeterService' },
},
});
bridge.startListening();Browser client:
import { WebSocketClient } from '@strata-js/rpcbridge-client';
const client = new WebSocketClient('http://localhost:3000/rpc');
const result = await client.request<{ message : string }>(
'greeter', 'greeter', 'hello',
{ name: 'World' }
);
console.log(result.message); // 'Hello, World!'HTTP with cURL
curl -X POST http://localhost:3000/rpc \
-H 'Content-Type: application/json' \
-d '{
"serviceName": "greeter",
"context": "greeter",
"operation": "hello",
"payload": { "name": "cURL" }
}'