Proxy Toggld through your own domain
Send every SDK request through a proxy on your own domain, with endpoint as a base URL. Paths, headers, WebSockets, and example proxies.
Some apps must send all of their traffic to their own domains, for example because their customers allow only those domains. Then put a proxy on your domain in front of the Toggld API, and point the SDKs at it.
Set endpoint
Both SDKs take endpoint, the base URL of the Toggld API (default https://api.toggld.io). Set it to the URL of your proxy. It can have a path prefix, with or without a trailing slash. The SDKs keep the prefix and add their paths after it.
import { ToggldClient } from "@toggld/js";
const flags = await ToggldClient.create({
sdkKey: process.env.TOGGLD_SDK_KEY!,
endpoint: "https://flags.example.com/toggld",
});import { ToggldClient } from "@toggld/browser";
const flags = await ToggldClient.create({
publicToken: "ffp_YOUR_PUBLIC_TOKEN",
endpoint: "https://flags.example.com/toggld",
context: { targetingKey: "user-123" },
});With this endpoint, the browser SDK sends POST https://flags.example.com/toggld/ofrep/v1/evaluate/flags and opens wss://flags.example.com/toggld/eval/v1/stream. The proxy removes /toggld and sends the rest to https://api.toggld.io. With @toggld/react, set endpoint on the ToggldClient that you pass to the provider. The serverless client and the toggld pull CLI (--api or TOGGLD_API_URL) take the same base URL.
Paths
Your proxy must pass these paths and methods. Each path comes after your prefix.
| Client | Method and path | Used for |
|---|---|---|
@toggld/js, sync mode (default) | GET /sync/v1/flags | Flag snapshot, with If-None-Match |
GET /sync/v1/stream (WebSocket) | Live updates | |
POST /v1/telemetry | Evaluation counts for Analytics | |
@toggld/js, remote mode | POST /ofrep/v1/evaluate/flags/{key} and POST /ofrep/v1/evaluate/flags | One flag, and every flag |
@toggld/js/serverless | POST /ofrep/v1/evaluate/flags | One bulk request per incoming request |
toggld pull | GET /sync/v1/flags | Download a flag file |
@toggld/browser, @toggld/react | POST /ofrep/v1/evaluate/flags | Values over HTTP |
GET /eval/v1/stream (WebSocket) | First values and live updates |
Headers
Pass these headers through unchanged:
| Direction | Headers |
|---|---|
| Request | Authorization (the server SDK's secret key, and the browser SDK's public token on HTTP), Content-Type, If-None-Match |
| Request, WebSocket | Upgrade, Connection, Sec-WebSocket-Key, Sec-WebSocket-Version, Sec-WebSocket-Protocol |
| Response | Content-Type, ETag, Retry-After, Cache-Control |
| Response, WebSocket | Sec-WebSocket-Protocol |
- The WebSocket credential is in
Sec-WebSocket-Protocol. A browserWebSocketcannot send anAuthorizationheader, so both SDKs send the key or token as a subprotocol entry,toggld.bearer.<token>, next totoggld.sync.v1(server SDK) ortoggld.eval.v1(browser SDK). The proxy must pass the header to Toggld, and pass back the subprotocol that Toggld selects in its answer. Do not log this header. - Do not forward your own cookies. The SDKs do not need them.
- Do not cache responses in the proxy. Toggld sets
Cache-Controlitself.
Browsers and CORS
A page on another origin than your proxy makes cross-origin requests. Then your proxy must answer CORS preflight (OPTIONS) requests for the OFREP paths: allow your page origin, the POST method, and the Authorization and Content-Type headers, and expose Retry-After. A proxy on the same origin as the page needs no CORS. WebSockets do not use CORS.
WebSockets
Both SDKs work through a proxy that cannot pass WebSockets, with slower updates:
@toggld/jsin sync mode keeps pollingGET /sync/v1/flags, every 30 seconds by default (sync.pollIntervalMs), and retries the stream with a backoff of up to about 30 seconds. Changes arrive with the next poll. To stop the stream attempts, setsync: { transport: "poll" }.@toggld/browserwaits up to 1.5 seconds for the stream, then asks over HTTP. It remembers for the browser session that the stream did not answer, and later pages use HTTP only. Without the stream there are no live updates: turn onpollIntervalMsto pick up changes on long-lived pages, or setstream: falseto skip the wait. See Live updates.- Remote mode and the serverless client use HTTP only.
A WebSocket stays open for a long time. Both SDKs send a heartbeat every 30 seconds, so an idle timeout of at least 60 seconds is enough.
Rate limits through a proxy
The Toggld API counts its per-address rate limits by the address it receives the request from, which is now your proxy. All traffic through the proxy shares those limits: 1,200 requests per minute for API requests from one address, and 120 evaluation requests per minute per environment for public tokens. Evaluation and sync requests with a secret key are exempt from the per-address limit; they share 30,000 requests per minute per environment. Browser traffic through one proxy address reaches the public token limit quickly. Plan for this before you send browser traffic through a proxy.
Example proxies
These are examples, not part of Toggld. Adapt them to your setup.
Cloudflare Worker
const UPSTREAM = "https://api.toggld.io";
const PREFIX = "/toggld";
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (!url.pathname.startsWith(`${PREFIX}/`)) return new Response("Not found", { status: 404 });
const upstream = new Request(new URL(url.pathname.slice(PREFIX.length) + url.search, UPSTREAM), request);
upstream.headers.delete("cookie");
// fetch also passes WebSocket upgrades: the 101 response carries the socket.
return fetch(upstream);
},
} satisfies ExportedHandler;Route it on your domain, for example flags.example.com/toggld/*.
nginx
# In the http block:
map $http_upgrade $connection_upgrade {
default upgrade;
"" close;
}
# In the server block of your domain:
location /toggld/ {
proxy_pass https://api.toggld.io/; # the trailing slash removes /toggld
proxy_http_version 1.1;
proxy_set_header Host api.toggld.io;
proxy_ssl_server_name on; # send SNI for api.toggld.io
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Cookie "";
proxy_read_timeout 120s; # WebSocket heartbeats come every 30 seconds
}