Decide where the call runs
The key is safe wherever the code that holds it never reaches a user’s device.SodaxProvider does new Sodax(config) inside your React tree, so any apiKey you pass it is a browser key. Configure it with a proxy instead — see Pattern 2.
Public env prefixes publish, they don’t protect
A server-side snippet likenew Sodax({ apiKey: process.env.SODAX_API_KEY }) reads undefined in a client component. The tempting fix is to rename the variable so the framework exposes it:
These prefixes are a publication mechanism, not an access mechanism. The build succeeds, the app works, and the key is now served to every visitor and baked into every bundle you have deployed. If you can read it in devtools → Sources, so can everyone else.
Use the prefixes for values that are meant to be public — a WalletConnect project ID, your own app’s origin — never for the SODAX key.
Pattern 1: keep the call on the server
If your app already renders on a server, the simplest fix is to make the SODAX call there and send the browser only the result. No proxy is needed.server-only package makes the build fail if a client component imports the module.
This works for reads, quotes and backend submissions. It does not help with anything the user’s wallet must sign in the browser — for that, the browser needs an SDK instance of its own, which is what Pattern 2 is for.
Pattern 2: proxy through your own backend
A backend-for-frontend (BFF) keeps the full SDK in the browser but points it at your own origin. Your server forwards each request to the SODAX gateway and attaches the key on the way out.The route handler
x-api-key the browser sends — in any casing — never reaches SODAX. The upstream is a constant, and the path must start with a prefix you chose. It derives the path from the request URL rather than from the route’s params, so it does not depend on the Next.js version.
The browser config
sodaxConfig to <SodaxProvider config={sodaxConfig}> or new Sodax(sodaxConfig). There is no apiKey in it, and none is needed: the proxy adds the key.
Both URLs are absolute because the SDK requires an http(s) URL, and a client component also renders on the server, where there is no window.location to resolve a relative path against.
The same shape anywhere
Nothing here is specific to Next.js. An Express or Hono route, a Cloudflare Worker, or an API gateway rule does the same job: accept a request under your mount, check the path against your allowlist, rebuild the headers withx-api-key from your server’s secret store, and forward to https://api.sodax.com/v1.
What the proxy has to cover
The SDK is not a single-route client, and oneapi.baseURL does not move all of it. A proxy that forwards /be and leaves the solver pointed at api.sodax.com is a silent half-migration: the app keeps working, and the calls you didn’t move go out without a key.
Sources: the packaged defaults are in
packages/types/src/common/constants.ts, and how each service resolves its base URL is in packages/sdk/src/backendApi/apiConfig.ts. See Configure SDK for the full layering.
Two details that decide the proxy’s shape:
- Mount the proxy at the gateway root, not the data API. Your
api.baseURLstands in forhttps://api.sodax.com/v1. A base URL ending in/beis treated as the legacy data-API mount: the SDK trims the suffix and logs a warning, so/api/sodax/bewould quietly be treated as/api/sodax: the app keeps working, but only a console warning tells you the base URL was rewritten. - Only
Content-TypeandAcceptneed forwarding. The SDK sends no other SODAX-specific request headers. It sendsContent-Type: application/jsonon every method, includingGET, so a proxy on a different origin gets a CORS preflight on every call. A same-origin proxy, like the route handler above, avoids that entirely.
Your proxy is now your problem
A forwarder that attaches your organisation’s key and accepts anything from anyone is an open relay for your quota. Before you ship it:- Authenticate or rate-limit it. Tie it to your own session or app check if you have one, and rate-limit per client either way.
- Forward only the paths you use. Trim
ALLOWED_PREFIXESto the services your app calls, and allow onlyGETandPOST. - Never forward to a URL the client chose. Keep the upstream a constant; don’t read a target host from a header, query parameter or body.
- Don’t log the key. Keep
x-api-keyout of request logs, error reports and traces, on the proxy and anything in front of it.
If you ship a key in the browser anyway
Sometimes there is no backend — a prototype, a static demo, a hackathon build. Shipping a key in the bundle is then a trade-off you can make knowingly, as long as you treat the key as public:- Give that app its own key, separate from your server’s, so rotating it touches nothing else. Every portal key carries the same scopes; there is no narrower “browser” key.
- Expect to rotate it. Anyone can copy it and send traffic that is attributed to your organisation and counts against your limits. Once enforcement is switched on, the copied key also passes the checks your own traffic relies on.
- Nothing else changes. Users still sign every transaction with their own wallets; the key gives nobody access to funds.
Storage checklist
- Read the key from an environment variable or a secret manager, never from source.
- Give each environment its own key — production and staging never share one.
- Keep
.envin.gitignore; put the key in your CI or host’s secret store, not in a Dockerfile or build log. - Keep it out of logs, error reports and issue trackers.
If a key has already shipped
- Rotate it with the two-key procedure: create the replacement, deploy it server-side, then revoke the published key.
- Remove it from the bundle. Move the call behind a server, rebuild without the key, and redeploy.
- Treat containment as a separate step. Revocation usually takes effect within about 15 seconds, with no guaranteed ceiling. Old bundles may stay in browser caches and CDNs after you redeploy, so purge your CDN, and don’t reuse the published key anywhere.
Sponsoring keys
The Stellar Sponsoring API uses its own key from a separate registry. Handle it exactly like a portal key — server-side, one per environment — and set it on its own config slice,api.sponsoringApiConfig.apiKey. Pointing api.baseURL at your proxy does not move sponsoring: its base URL and headers never inherit, so proxy it separately if you call it from a browser.