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:
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.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.
explorer appThe Honeycluster monorepo ships an explorer that already does this. Use it as a template.
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.
TypeScriptfunction 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:
TypeScriptasync 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.
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:
TypeScriptconst 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:
TypeScriptproxyRouter.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.
If you're not using Honeycluster's monorepo, the pattern is three components:
X-API-KEY header attached, stream the
response back.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.
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.