ZuploZuplo
LoginStart for Free
  • Documentation
  • API Reference
Introduction
Getting Started
    Develop in the portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingDynamic MCP Server - Quickstart
    Develop locally with the CLI
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingDynamic MCP Server - Quickstart
Concepts
Development
Policies
Handlers
    URL ForwardURL RewriteFunction HandlerAWS LambdaMCP ServerRedirectOpenAPI Spec HandlerInternal Route HandlersWebSocket HandlerWebSocket Pipeline Handler
API Keys
Rate Limiting
Caching
MCP Server
MCP Gateway
AI Gateway
Developer Portal
Monetization
GraphQL
Deploying & Source Control
Analytics
Observability
Networking & Infrastructure
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsVersion Support PolicySecuritySupportTrust & ComplianceChangelog
powered by Zudoku
Handlers

WebSocket Pipeline Handler

The WebSocket Pipeline Handler is an Enterprise-only feature. Please contact us to trial this or sign up for an Enterprise account.

The webSocketPipelineHandler is Zuplo's WebSocket handler. It proxies WebSocket connections to your backend and optionally lets you run code at two points in the connection's life:

  • Message policies pass every WebSocket message through a pipeline of policy functions before forwarding it. Each policy can inspect, transform, or drop the message. Use this to redact sensitive fields, enforce a message schema, filter events, or add observability to real-time traffic.
  • The onOpen connection hook runs once per connection, after the connection to your backend is established and before any message is relayed. Use it to gate connections, send a handshake message to your backend, or attach your own connection lifecycle listeners (such as close).

With no policies and no onOpen hook configured, the handler is a transparent passthrough — this is what the deprecated webSocketHandler did, and it now resolves to this handler.

Messages are intercepted in both directions, configured independently:

  • inbound policies process messages traveling from the client to your backend.
  • outbound policies process messages traveling from your backend to the client.

The pipeline only intercepts message events. Connection lifecycle events are handled automatically: when either side closes, the other side is closed, and socket errors are logged and the connection is torn down. There is no dedicated policy hook for close or error events — instead, use the onOpen hook to attach your own listeners to either socket.

Configuration

Use the webSocketPipelineHandler export and add an inbound and/or outbound array under options.policies. Each entry points to an exported function in one of your project's modules.

Code
"/my-websocket": { "x-zuplo-path": { "pathMode": "open-api" }, "get": { "summary": "WebSocket route with message interception", "x-zuplo-route": { "corsPolicy": "none", "handler": { "export": "webSocketPipelineHandler", "module": "$import(@zuplo/runtime)", "options": { "rewritePattern": "https://myservice.com/websocket", "policies": { "inbound": [ { "module": "$import(./modules/websocket-policies)", "export": "redactInbound" } ], "outbound": [ { "module": "$import(./modules/websocket-policies)", "export": "filterOutbound" } ] } } }, "policies": { "inbound": [] } }, "operationId": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e" } }

Two different policies blocks

The policies object inside handler.options configures the message policies that run on each WebSocket frame. This is separate from the route-level policies block (the inbound array next to handler), which configures the standard request policies — such as API Key or Rate Limiting — that run once during the initial connection upgrade.

The rewritePattern option behaves identically to the WebSocket Handler, including JavaScript string interpolation.

Writing a message policy

A message policy is an exported function that matches the following signature:

Code
import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; async function webSocketPolicy( data: string, target: WebSocket, source: WebSocket, request: ZuploRequest, context: ZuploContext, ): Promise<unknown>;
ParameterDescription
dataThe message payload. For text protocols this is a string; binary frames arrive in the platform's binary form (for example, an ArrayBuffer). For an inbound policy this is the message from the client; for an outbound policy it is the message from the backend.
targetThe destination socket the message is being forwarded to. For inbound this is the backend connection; for outbound it is the client connection.
sourceThe socket the message originated from. Call source.send(...) to send a message back to the originator, such as an acknowledgement or error.
requestThe original ZuploRequest from the connection upgrade.
contextThe ZuploContext for the connection.

The return value controls what happens next:

  • Return the data (modified or unchanged) to forward it to target. When multiple policies are configured, the return value is passed as data to the next policy in the array.
  • Return undefined to drop the message. It is not forwarded, and no further policies run for that message.

Policies run in the order they appear in the inbound / outbound array, and each policy may be asynchronous. If a policy throws, Zuplo logs the error and drops the message.

Example: redact fields from inbound messages

This inbound policy parses each JSON message from the client, removes a sensitive field, and forwards the result to the backend.

Code
import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export async function redactInbound( data: string, target: WebSocket, source: WebSocket, request: ZuploRequest, context: ZuploContext, ) { let message: Record<string, unknown>; try { message = JSON.parse(data); } catch (err) { context.log.warn("Dropping non-JSON WebSocket message"); return undefined; // drop messages that aren't valid JSON } delete message.ssn; return JSON.stringify(message); }

Example: filter outbound messages

This outbound policy inspects messages from the backend and drops internal events so they never reach the client. Other messages pass through unchanged.

Code
import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; export async function filterOutbound( data: string, target: WebSocket, source: WebSocket, request: ZuploRequest, context: ZuploContext, ) { const message = JSON.parse(data); if (message.type === "internal") { return undefined; // drop internal events; the client never sees them } return data; }

You can configure multiple policies in each direction to compose behavior — for example, one policy to validate a message against a schema and a second to redact fields. Because each policy receives the previous policy's output, order matters.

The onOpen connection hook

The onOpen option configures a function that runs once per connection, after both the client and backend connections are established and before any message is relayed. Unlike message policies, it isn't tied to a direction — it receives both sockets.

Configure it at the options level, as a sibling of rewritePattern and policies. It works with or without message policies:

Code
"handler": { "export": "webSocketPipelineHandler", "module": "$import(@zuplo/runtime)", "options": { "rewritePattern": "https://myservice.com/websocket", "onOpen": { "module": "$import(./modules/websocket-hooks)", "export": "onWebSocketOpen" } } }

The hook matches the WebSocketOnOpenHook type exported from @zuplo/runtime — the same parameters as a message policy, minus the message data:

Code
import { ZuploContext, ZuploRequest } from "@zuplo/runtime"; type WebSocketOnOpenHook = ( target: WebSocket, source: WebSocket, request: ZuploRequest, context: ZuploContext, ) => void | Promise<void>;
ParameterDescription
targetThe backend connection. Call target.send(...) to send a message to your backend.
sourceThe client connection. Call source.send(...) to send a message to the client.
requestThe original ZuploRequest from the connection upgrade.
contextThe ZuploContext for the connection.

The hook is awaited, and its outcome controls the connection:

  • Return (or resolve) to accept the connection. Message relaying begins only after the hook completes — messages that arrive while it runs are buffered and delivered afterward, in order.
  • Throw (or reject) to abort the connection. Both sockets are closed and the client never receives the 101 Switching Protocols upgrade response.

Example: gate connections and greet the backend

This hook rejects unauthenticated connections, then sends a handshake message to the backend before any client traffic flows.

Code
import { WebSocketOnOpenHook } from "@zuplo/runtime"; export const onWebSocketOpen: WebSocketOnOpenHook = async ( target, source, request, context, ) => { if (!request.user) { // Aborts the upgrade: both sockets close and no 101 is returned throw new Error("Unauthenticated WebSocket connection"); } // Runs before any client message is relayed to the backend target.send( JSON.stringify({ type: "session.start", user: request.user.sub }), ); };

Example: react to the connection closing

There is no separate onClose option — because onOpen hands you both raw sockets, attach a standard close event listener to either one. Listeners attached here live for the duration of the connection; there's no need to remove them.

Code
import { WebSocketOnOpenHook } from "@zuplo/runtime"; export const trackConnection: WebSocketOnOpenHook = ( target, source, request, context, ) => { const connectedAt = Date.now(); // Fires when the client disconnects source.addEventListener("close", () => { const seconds = Math.round((Date.now() - connectedAt) / 1000); context.log.info(`Client disconnected after ${seconds}s`); }); // Fires when your backend closes the connection target.addEventListener("close", () => { context.log.warn("Backend closed the WebSocket connection"); }); };

The same pattern works for error and message events. For a listener that should fire only once — such as waiting for a single handshake reply from your backend — pass { once: true } as the third argument to addEventListener.

Performance

With no message policies and no onOpen hook, the gateway hands the connection straight through to your backend and stays out of the per-message data path entirely. Configuring onOpen (or any message policy) keeps the gateway in the data path for that route, since the hook needs both live sockets to act on.

Edit this page
Last modified on July 29, 2026
WebSocket HandlerOverview
On this page
  • Configuration
  • Writing a message policy
  • Example: redact fields from inbound messages
  • Example: filter outbound messages
  • The onOpen connection hook
    • Example: gate connections and greet the backend
    • Example: react to the connection closing
JSON
TypeScript
TypeScript
TypeScript
JSON
TypeScript
TypeScript
TypeScript