blockchainsv
Web3 Integration & APIs·July 22, 2026·14 min read

Blockchain API key security: frontend vs. backend proxy

A Web3 frontend can look perfectly healthy until an RPC bill spikes, a rate limit starts throttling real users, or a teammate notices that the production provider key is sitting in DevTools.

Blockchain API key security: frontend vs. backend proxy

This is one of those problems that feels small at first: “It is only a read-only endpoint.” Then eth_getLogs enters the picture, an attacker reuses the key outside the browser, and the gap between a convenient integration and an expensive public utility becomes painfully clear.

The hard truth is uncomplicated: a blockchain API key shipped to a browser is not a secret. It is available in the application bundle, the Network tab, and often the repository history if the team had a bad day with environment files. We cannot hide a blockchain API key in a frontend application. What we can do is reduce the blast radius, put the sensitive capability behind server-side controls, and make abuse expensive to sustain rather than cheap to automate.

For most production dApps, that means choosing deliberately between direct frontend RPC access with strict provider rules and a Backend for Frontend (BFF) proxy. These are not interchangeable patterns. They protect different things, create different operational work, and affect the user journey in different ways.

The browser is not a secret store

The usual path to exposure is familiar. We initialize ethers, viem, wagmi, or an SDK with an RPC URL, deploy the app, and see a value that appears to be tucked away in an environment variable.

It is not tucked away.

Variables such as NEXT_PUBLIC_RPC_URL, VITE_RPC_URL, and REACT_APP_ALCHEMY_KEY are build-time inputs for client code. Their prefixes are effectively a disclosure label: the bundler includes them in files delivered to every visitor. Even if the key is not visible as a neat string in the source map, the browser must send it somewhere. Anyone can inspect the request and copy the endpoint.

The same applies to mobile clients. A key embedded in an app binary can be extracted. Obfuscation may make a casual scrape less convenient, but it does not create a trust boundary. Nor does CORS. CORS controls what a browser is allowed to read across origins; it does not stop someone from taking an exposed RPC URL and calling it through curl, Postman, a script, or an infrastructure provider of their own.

That distinction matters because a public RPC endpoint can be abused in ways that have nothing to do with our UI:

  • Attackers can replay cheap read calls at high volume and exhaust rate limits shared by legitimate users.
  • They can send expensive historical log queries, particularly broad eth_getLogs requests across large block ranges.
  • They can enumerate contract state or wallet activity at a volume that distorts our usage metrics.
  • They can turn a paid key into a free backend for their own bot, dashboard, or scraping job.
  • They can create noisy failures that look like a frontend state-sync bug when the actual issue is upstream quota exhaustion.

On providers that meter Compute Units rather than raw requests, the cost difference is not theoretical. Lightweight reads may cost roughly 1–10 CUs, while heavy calls such as eth_getLogs can run to 150–300+ CUs. A single exposed key therefore has two separate risks: availability for our users and a bill that tracks someone else’s workload.

A key visible to the browser is a routing identifier, not a credential we can rely on for security.

This does not mean every frontend RPC URL is automatically a catastrophe. Public, rate-limited endpoints have a place, especially for prototypes and low-stakes read paths. But calling a key “secret” after it enters the client bundle is where teams start designing around a false premise.

Frontend exposure versus a backend proxy

The comparison becomes clearer when we stop asking which option is “more secure” in the abstract and ask what each architecture can enforce.

ParameterDirect frontend RPC accessBackend for Frontend proxy
Where the provider key livesIn the user-accessible client request pathIn server-side environment variables or a secrets manager
Can the key be copied and reused externally?YesNot directly
Identity available to apply policyMostly browser origin and provider-side rulesUser session, application route, IP signals, product-specific context
Request shapingLimited to provider controlsFull control: allowlists, pagination, range caps, caching, queues
Operational complexityLowModerate: deploy, observe, secure, and scale a service
Best fitPublic reads, prototypes, resilient fallback pathsPaid APIs, expensive queries, privileged workflows, production data flows
Failure modeProvider quotas can affect all users at onceProxy can become a dependency, so it needs graceful degradation

Direct access keeps the path short: browser to RPC provider. That can be a reasonable choice when the requests are low-cost, the data is public, and the provider supports meaningful restrictions. It is also useful for graceful degradation. If our indexer-backed activity page is temporarily unavailable, allowing the wallet UI to perform a limited public eth_call can preserve a useful user journey.

A BFF changes the trust model. The browser calls an endpoint we own, such as /api/chain/balances or /api/portfolio/activity. The server holds the provider credential and calls the RPC provider or indexer. Crucially, the frontend no longer gets a generic, reusable gateway into our provider account.

That is the difference worth paying attention to. A well-designed proxy is not merely “an RPC URL with the key removed.” It is where we translate product intent into bounded blockchain queries.

Instead of accepting arbitrary JSON-RPC payloads, it can expose purpose-built operations:

  • GET /api/token-balances?address=… with address validation, supported-chain checks, and caching.
  • GET /api/activity?wallet=…&cursor=… backed by an indexer rather than an unbounded log scan.
  • POST /api/quote where the server validates input before it reaches a pricing or simulation provider.
  • GET /api/contract-state that allows a known set of contract addresses and read methods.

This design avoids the most common proxy mistake: creating /api/rpc, forwarding any method and parameters unchanged, and assuming the presence of a server means the problem is solved. We have hidden the key in that setup, yes. But we may still have built a public relay that lets anyone spend our Compute Units.

What a useful BFF actually does

Let’s build the mental model as a collaborative debugging session. A user opens a portfolio view. The frontend needs token balances, recent activity, and a couple of contract reads. The naive version triggers several provider calls directly from React hooks, possibly again every time a component remounts or a wallet reconnect causes state sync to wobble.

A BFF lets us decide what that page is allowed to cost.

First, the provider credential stays on the server. It belongs in a server-side environment variable for a small deployment, or in a managed secrets system as the application grows. It does not belong in the Git repository, in a public runtime config object, or in an image layer that every debugging tool can inspect.

Second, the BFF authenticates or at least classifies the caller. Not every read endpoint needs a login, but the proxy should know whether a request comes from an anonymous visitor, an authenticated account, an internal job, or a trusted webhook. That gives us room to set different limits and prevents a single anonymous path from consuming the same budget as signed-in users.

Third, it constrains the query. This is the part that consistently improves both security and UX:

1. Allow known chains, contracts, methods, and block ranges. If the page only reads a token balance on Base and Ethereum mainnet, the endpoint should not accept a request for any arbitrary chain and contract. If we need logs, cap the block span and paginate deliberately.

2. Use semantic endpoints instead of raw JSON-RPC forwarding. A request for “NFT activity for this wallet” is a product operation. It should not become unrestricted access to eth_getLogs with user-controlled topics and a genesis-to-head range.

3. Cache public and repeatable reads. Block-aware caching is particularly effective. We can cache a response for the latest observed block, invalidate or refresh when the head advances, and avoid asking the node the same question for every browser tab.

4. Rate-limit by the right dimension. IP-only limits can punish users behind shared networks. Wallet address alone can be gamed before signature-based authentication. In practice, we often combine route-level limits, session identity where available, and a modest anonymous budget.

5. Set timeouts and response-size limits. A backend proxy should fail predictably. If an upstream provider stalls, the frontend needs a clear loading or retry state, not an indefinitely pending request that makes the whole dashboard feel broken.

6. Observe the expensive edges. Record method names, chain IDs, response time, cache status, error category, and estimated provider cost where the provider exposes it. Do not log credentials or raw sensitive payloads. The point is to spot abnormal query shapes before finance does.

The proxy must also have strict CORS configuration. Allow the specific frontend origin or origins that need it; do not return a permissive wildcard policy out of habit, especially for endpoints that rely on browser credentials. This is useful browser-side protection, but it is not the primary defense. A server endpoint still needs authentication, rate limits, and request validation because non-browser clients do not obey CORS.

There is a trade-off. The BFF adds another network hop and another service to operate. We should not invent exact latency numbers because the result depends on geography, cache-hit rate, provider location, and runtime behavior. But from a user perspective, the bigger risk is usually not the extra hop. It is a frontend that fans out into too many uncached provider calls and turns minor RPC variance into visible UI friction.

A proxy can actually make the experience calmer: fewer round trips, one page-oriented response, consistent retry behavior, and a fallback message that tells users whether the chain is slow, the wallet is disconnected, or the data is still indexing.

Provider restrictions are valuable—but they are a second layer

There are cases where direct frontend access is intentional. Maybe we are shipping a static, decentralized frontend. Maybe the dApp needs a read path that does not depend on our API. Maybe the application is early enough that the operational cost of a BFF is disproportionate.

In those cases, provider-level access rules are worth configuring. They are not cosmetic toggles.

Domain allowlisting can restrict a key to requests carrying an approved browser Origin. IP allowlisting is useful for server workloads, cron jobs, and indexers. Some providers can also restrict allowed contracts or wallet addresses, which can narrow the scope of requests further.

There are a few details that repeatedly catch teams:

  • Domain allowlisting requires the browser to send an Origin header. If the header is absent, a provider configured to require an approved origin may reject the request.
  • A wildcard such as *.example.com covers subdomains but not necessarily example.com itself. Add the parent domain explicitly if both are in use.
  • Preview deployments, local development, staging, and production often need separate keys and separate allowlists. Reusing production credentials in a preview environment is an easy way to create confusing failures.
  • Domain restrictions should not be treated as cryptographic proof of caller identity. Outside normal browser conditions, headers can be forged. They reduce casual reuse; they do not replace a server-side trust boundary.

Infura’s API-key-secret mode illustrates why the details matter. Requiring a secret for every request adds authentication headers, which makes sense for server-to-server use. But a frontend-only application calling such a setup directly can run into CORS failures because the browser request and authentication flow do not receive the expected CORS response headers. The fix is not to weaken the secret requirement until the frontend works. The fix is architectural: place a backend between the browser and the credentialed provider call.

Provider restrictions make an exposed key less convenient to abuse. A BFF makes the key unavailable to abuse in the first place.

The strongest practical setup is often hybrid. Use the BFF for paid, high-volume, privileged, or query-heavy data paths. Keep tightly restricted provider keys for deliberate client-side resilience paths. Use a separate unprivileged public endpoint when a wallet connection library needs a default transport before the application session is established.

We do not need to be doctrinaire about Web2 infrastructure here. A modest API route, edge function, or containerized service is not a betrayal of decentralization. It is a way to keep the frontend from carrying a capability it cannot protect.

Preventing CU abuse before it becomes an incident

Compute Unit billing changes how we should think about abuse. Request-count limits are not enough when one expensive RPC method can consume the budget of dozens or hundreds of small reads.

The practical defense is to identify high-cost methods and design them out of browser-controlled workflows where possible. eth_getLogs is the classic example. It is powerful, necessary, and easy to misuse. A broad log query can become costly even when it returns little useful data.

For event-heavy product features, we should prefer an indexed data layer over repeatedly scanning raw logs from the UI. A subgraph, a custom indexer, or a provider’s enhanced API can transform “search the chain from some old block until now” into a paginated application query. That also gives us a stable schema, cursor-based pagination, and a much nicer path for handling reorganizations and partial indexing.

When raw logs are genuinely required, the BFF should enforce discipline:

  • Limit the maximum block interval per request and paginate internally.
  • Permit only known event signatures and contract addresses.
  • Reject unbounded fromBlock values.
  • Cache recent ranges where multiple users are likely to ask the same question.
  • Set a concurrency ceiling for expensive routes instead of letting a traffic burst create a provider-side stampede.
  • Return a partial or delayed state in the frontend when data is still processing, rather than retrying aggressively from every mounted component.

That last point sounds mundane, but it protects the user journey. A frontend retry loop can be indistinguishable from an attack at the provider level. If the interface automatically refetches on focus, reconnect, block update, and component remount, we may be generating our own rate-limit incident.

We should also rotate credentials on a schedule rather than waiting for evidence of a leak. A 90-day rotation cadence is a sensible operational baseline for many teams, provided it is automated enough not to become a quarterly outage. Use separate keys by environment and workload: production frontend fallback, BFF production, staging, local development, background indexing. A key that has one job is easier to revoke without breaking every surface at once.

If we suspect exposure, the response is direct: create a replacement key, update the server secret or deployment configuration, revoke the old key, inspect provider usage for abnormal methods and origins, and review repository history and build artifacts. Trying to “hide” the old client-side key with a new frontend release does not work; copies of the previous build and network records may remain available.

The right architecture follows the cost of a request

The most useful question is not “Can we keep this blockchain API key in the frontend?” We cannot, in the security sense. The better question is: what can this request do if someone controls it at scale?

If the answer is “fetch a public block number through a key with a narrow domain restriction,” direct access may be an acceptable product trade-off. If the answer is “run expensive historical queries, reach a paid API, expose account-specific data, or consume a shared production quota,” it belongs behind a BFF.

That boundary improves more than credential security. It gives us a place to stabilize state sync, cache data close to the product, normalize provider errors, and ensure that our users see a working interface rather than the raw turbulence of the RPC layer.

Web3 UX already has enough friction: wallet permissions, chain switches, indexing delays, and transaction confirmation. We do not need to add leaked credentials and avoidable quota failures to the list. Put secrets where the browser cannot retrieve them, constrain the requests your product actually needs, and let provider allowlists support that design rather than carry it alone.

FAQ

Why can't I just hide my blockchain API key in an environment variable?
Environment variables prefixed for frontend frameworks are included in the application bundle delivered to the user's browser, making them visible to anyone who inspects the code or network requests.
What are the risks of exposing a blockchain API key in the frontend?
Exposed keys can be reused by attackers to exhaust your rate limits, run expensive historical queries that inflate your costs, or distort your usage metrics.
What is the main advantage of using a Backend for Frontend (BFF) proxy?
A BFF keeps your provider credentials on the server and allows you to translate product intent into bounded, validated queries, preventing users from making arbitrary or expensive RPC calls.
Can I rely on CORS to protect my RPC endpoint?
No, CORS only restricts what a browser can read across origins; it does not prevent an attacker from using tools like curl or Postman to call your RPC URL directly.
How should I handle expensive queries like eth_getLogs?
You should avoid raw log scans from the frontend and instead use an indexed data layer or a proxy that enforces limits on block ranges, event signatures, and request frequency.

By Chloe Redfern