Proxying Requests
Why browser apps must proxy Honeycluster traffic through a backend you control, and how the explorer app implements the pattern end-to-end.

The public Honeycluster endpoint (honeycluster.io) is open and keyless, so browsers can talk to it directly. You only need a server-side proxy in a few specific cases:

  1. You're on a private / enterprise plan that uses an API key. API keys are project-wide credentials and must never ship to the browser — not in a build, not in an env var exposed to Vite, not in a cookie readable by JavaScript.
  2. You're on a private plan and need WebSocket access from browsers. The Web Platform's WebSocket API accepts only a URL and a subprotocol list — there's no way to attach X-API-Key on the upgrade handshake from browser JavaScript, so the only way to carry auth to a private WSS endpoint is to proxy through your own backend.
  3. You want to rate-limit your own users, pre-authorize them against your application's session system, or inject per-user audit logs before letting them spend against Honeycluster's shared tier.

If none of these apply — you're just calling the public cluster from a client app — connect directly to https://honeycluster.io / wss://honeycluster.io and skip this page.

A thin proxy inside your own backend solves all three cases: the key (if any) lives in the server's environment, the server owns the upstream connection, and the browser only talks to your origin.

Reference implementation: the explorer app
##

The Honeycluster monorepo ships an explorer that already does this. Use it as a template.

Frontend: talk to your own origin
###

packages/apps/explorer/src/lib/xrpl/provider.tsx builds the WebSocket URL dynamically. In development it points at a local proxy route on the same host; in production it points at the public Honeycluster endpoint.

TypeScript
function getWsUrl(): string {
  if (isProduction) {
    return `wss://${getUpstreamDomain()}`
  }
  const wsProto = window.location.protocol === 'https:' ? 'wss' : 'ws'
  return `${wsProto}://${new URL(config.server.api).host}/proxy/xrpl-ws?network=${getNetwork()}`
}

HTTP RPC follows the same pattern — the browser POSTs to /proxy/xrpl-rpc and the server forwards to the upstream:

TypeScript
async function httpRpc(method: string, params: Record<string, unknown> = {}) {
  const res = await fetch(getRpcUrl(), {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ method, params: [params] }),
  })
  return res.json()
}

Notice what's missing: no X-API-Key header anywhere. The browser never sees the key.

Backend: relay with the key attached
###

packages/apps/api-server/src/proxy/xrpl-proxy.ts runs a WebSocket listener at /proxy/xrpl-ws. When a browser connects, the server opens an upstream socket to Honeycluster with the key injected, then pipes frames in both directions:

TypeScript
const providerWs = new WebSocket(provider.url, {
  ...(provider.apiKey ? { headers: { 'X-API-KEY': provider.apiKey } } : {}),
})

clientWs.on('message', (msg) => providerWs.send(msg))
providerWs.on('message', (msg) => clientWs.send(msg))

clientWs.on('close', () => providerWs.close())
providerWs.on('close', () => clientWs.close())

HTTP RPC proxying is shorter — just forward the body and inject the key:

TypeScript
proxyRouter.post('/xrpl-rpc', async (req, res) => {
  const net = getNetworkConfig(req.query.network)
  const upstream = await fetch(net.httpUrl, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...(net.apiKey ? { 'X-API-KEY': net.apiKey } : {}),
    },
    body: JSON.stringify(req.body),
  })
  const payload = await upstream.json()
  res.json(payload)
})

The api-server also provides /proxy/xrpl-unl, /proxy/xrpl-amendments, and /proxy/xrpl-toml routes with the same shape — all server-side, all attaching X-API-KEY before forwarding.

Building your own proxy
##

If you're not using Honeycluster's monorepo, the pattern is three components:

  1. A WebSocket relay on your backend. On connect, open an upstream WebSocket with the API key in the headers. Pipe frames both ways. Handle close events on both sides so neither connection leaks.
  2. An HTTP forwarder for REST endpoints. Read the incoming request body, forward it with the X-API-KEY header attached, stream the response back.
  3. Your own authorization. Before the forwarder runs, verify that the caller is allowed to burn Honeycluster credits — a session cookie, a JWT, an internal bearer token, whatever your auth system uses. Without this step, any browser on the internet can spend your credits.

The Build a Node.js API Proxy tutorial walks through a minimal Express version of (2) and (3). For the WebSocket relay in (1), study the explorer's xrpl-proxy.ts — it's the canonical pattern.

What about query-string tokens?
##

Some providers let clients carry a token in the WebSocket URL: wss://…?token=abc. Honeycluster does not — query-string tokens show up in access logs, Referer headers, browser history, and intermediate proxies, so we treat them as inherently leaked.

Always proxy.

TL;DR

Public endpoint: connect directly from anywhere, including the browser. No key needed.

Private / enterprise endpoint from Node: connect directly, attach X-API-Key in headers.

Private / enterprise endpoint from the browser: proxy through your own backend. The browser never holds the key; your backend holds the key and opens the upstream connection.