Skip to content

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

bash
# Server
npm install @strata-js/rpcbridge

# Client (browser or Node)
npm install @strata-js/rpcbridge-client

Server

Standalone Usage

The simplest way to run the bridge is to let it create its own Express and Socket.IO server:

typescript
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:

typescript
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:

typescript
interface ServerConfig
{
    server : WebServerConfig;
    strata : StrataClientConfig;
    globalBlockList : string[];
    services : ServiceConfig;
}

WebServerConfig

typescript
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:

typescript
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:

  1. globalBlockList is checked first. If the call matches, it is denied.
  2. If the service has an allowList, only calls matching the list are permitted. Everything else is denied.
  3. If the service has a blockList (and no allowList), calls matching the list are denied. Everything else is permitted.
  4. allowList takes precedence -- if both are defined, blockList is ignored.
typescript
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:

json
{
    "serviceName": "users",
    "context": "profile",
    "operation": "get",
    "payload": { "userId": "abc123" },
    "meta": {},
    "auth": "Bearer ...",
    "timeout": 5000
}
FieldTypeRequiredDescription
serviceNamestringyes*Key from the services config map.
serviceGroupstringyes*Direct service group name (requires allowServiceGroupOverride).
contextstringyesTarget context name.
operationstringyesTarget operation name.
payloadobjectyesRequest payload.
metaobjectnoMetadata passed through to the service.
authstringnoAuthentication token.
timeoutnumbernoRequest 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:

typescript
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:

typescript
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.

typescript
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

typescript
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

typescript
new WebSocketClient(uri : string, opts ?: ClientConstructorOptions)
ParameterTypeDescription
uristringThe bridge server URL including the path (e.g. http://localhost:3000/api).
optsClientConstructorOptionsSocket.IO client options plus an optional emitTimeout. Defaults to { transports: [ 'websocket' ], reconnection: true, emitTimeout: 20000 }.

request()

typescript
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.

ParameterTypeRequiredDescription
serviceNamestringyesThe service name as configured in the bridge's services map.
contextstringyesTarget context.
operationstringyesTarget operation.
payloadRecord<string, unknown>yesRequest payload.
metaRecord<string, unknown>noMetadata.
authstringnoAuthentication token.
timeoutnumbernoOverride the default emitTimeout.

Error Handling

The client throws typed errors:

  • RemoteServiceError -- the bridge returned a 'failed' status. Contains code, message, name, stack, and optional details from the remote service.
  • EmitTimeoutError -- the Socket.IO emit timed out waiting for an acknowledgement.
typescript
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.):

typescript
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):

typescript
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):

typescript
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:

typescript
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

bash
curl -X POST http://localhost:3000/rpc \
    -H 'Content-Type: application/json' \
    -d '{
        "serviceName": "greeter",
        "context": "greeter",
        "operation": "hello",
        "payload": { "name": "cURL" }
    }'