Skip to content

Transports ​

A transport connects an incoming communication channel to the Capix execution engine. The same capability registry can be served over multiple transports simultaneously.

Available transports ​

TransportPackageProtocol
REST@capixjs/transport-restHTTP/1.1
WebSocket@capixjs/transport-wsWebSocket
GraphQL@capixjs/transport-graphqlHTTP/1.1 (GraphQL over HTTP)
Queue@capixjs/transport-queueCustom (adapter-based)
MCP@capixjs/transport-mcpModel Context Protocol (stdio / Streamable HTTP)

Multiple transports ​

ts
import { createServer } from '@capixjs/core';
import { restTransport } from '@capixjs/transport-rest';
import { wsTransport } from '@capixjs/transport-ws';
import { graphqlTransport } from '@capixjs/transport-graphql';

createServer({
  context:      buildContext,
  capabilities: { users, posts },
  transports: [
    restTransport({ port: 3000 }),
    wsTransport({ port: 3001 }),
    graphqlTransport({ port: 4000 }),
  ],
}).start();

All three transports serve the same capabilities. The execution engine is shared.

Per-transport capabilities ​

Pass capabilities directly to a transport to override which capabilities it exposes:

ts
const publicAPI = { items: { list: listItems, get: getItem } };
const memberAPI = { items: { create: createItem, update: updateItem } };
const jobsOnly  = { jobs: { processItem, generateReport } };

createServer({
  context:      buildContext,
  capabilities: { ...publicAPI, ...memberAPI }, // default for REST + GraphQL
  transports: [
    restTransport({ port: 3000 }), // uses server-level capabilities
    graphqlTransport({ port: 4000, capabilities: publicAPI }), // GraphQL only exposes public API
    queueTransport({ queues: ['jobs'], adapter, capabilities: jobsOnly }), // queue only
  ],
});

If every transport specifies its own capabilities, the top-level field can be omitted. Capix throws at startup if a transport has no capabilities and no server-level default is provided.

Graceful shutdown ​

server.stop() drains every transport instead of dropping connections:

  • HTTP transports (REST, GraphQL, MCP) stop accepting new connections, drop idle keep-alive sockets immediately, give in-flight requests a drain window, then force-close whatever remains.
  • WebSocket sends every client a clean close frame (1001 going away) and terminates sockets that never finish the close handshake.
  • Queue adapters finish the jobs they are processing before workers close.

The drain window is shutdownTimeoutMs on each transport (default 10_000):

ts
restTransport({ port: 3000, shutdownTimeoutMs: 5_000 })

Wire it to process signals for zero-downtime deploys:

ts
const server = createServer({ ... });
await server.start();
process.on('SIGTERM', () => void server.stop().then(() => process.exit(0)));

Custom HTTP transports get the same behavior from closeHttpServerGracefully(server, drainMs) in @capixjs/core.

What transports can and cannot do ​

Transports can:

  • Parse incoming requests and extract the capability name, input, and headers
  • Pass headers to buildContext via the RawRequest
  • Forward the structured response to the caller
  • Apply transport-specific features (file upload, subscriptions, batching)

Transports cannot:

  • Skip guards
  • Access the resolver directly
  • Bypass input validation

Every transport goes through the same execution engine. Guards always run. Input is always validated. This means adding a transport never creates a security hole.

Custom transports ​

A transport is an object with a mount(invoke, options) method:

ts
import type { Transport, InvokeFn, MountOptions } from '@capixjs/core';

const myTransport: Transport = {
  async mount(invoke: InvokeFn, options: MountOptions) {
    // Set up your server/listener here
    // Call invoke() for each incoming request

    const response = await invoke({
      capability: 'users.getUser',
      input: { id: '1' },
      headers: { authorization: 'Bearer token' },
    });

    // response.ok, response.status, response.data or response.error
    return async () => {
      // Teardown — called by server.stop()
    };
  },
};

For per-transport capabilities, implement TransportWithCapabilities:

ts
import type { TransportWithCapabilities, GroupTree } from '@capixjs/core';

function myTransport(options: { capabilities?: GroupTree }): TransportWithCapabilities {
  return {
    _capabilities: options.capabilities,
    async mount(invoke, { registry }) {
      // ...
    },
  };
}

Lifecycle ​

createServer(config) builds the server but does not start any transports. Call .start() to mount all transports. Call .stop() to unmount them in the reverse order they were started.

ts
const server = createServer({ ... });
await server.start();

process.on('SIGTERM', async () => {
  await server.stop();
  process.exit(0);
});

Released under the MIT License.