Header Logo

Preventing API Key Leakage in React and Next.js Realtime Avatar Apps

Preventing API Key Leakage in React and Next.js Realtime Avatar Apps

Prevent API key leakage in React and Next.js realtime avatar apps with server-side sessions, scoped tokens, and safer env handling.

Introduction


When you build a React or Next.js app with realtime avatars, the failure mode is usually not the avatar pipeline itself. It’s key handling. Somewhere between “just try it in the browser” and “ship the production embed,” an API key ends up in client code, a bundle, a source map, a network trace, or a support screenshot.


If your app talks to a backend that creates avatar sessions, negotiates realtime media, or manages usage, the browser should not hold a long-lived secret. By the end of this post, you should be able to identify the common leakage paths in React and Next.js, choose the right server/client split, and implement a safer pattern for session creation and embedded realtime avatars.


Why API keys leak in React and Next.js


The mistake is usually architectural, not cryptographic. React apps are good at rendering UI; they are not a secret store. Any value compiled into frontend code is recoverable by users, browser extensions, or proxies. In Next.js, it’s easy to accidentally blur the boundary because the same repository often contains both server and client code.


Typical leakage paths:


  • Build-time environment injection. Variables prefixed with NEXT_PUBLIC_ are intentionally shipped to the browser.

  • Client-side fetches to privileged APIs. If the browser directly calls an API that expects Authorization: Bearer sk_live_..., the key is exposed.

  • Accidental logging. Request headers, error objects, and debug output can end up in browser logs or telemetry.

  • Hydration and props leakage. Passing secrets through serialized props to client components makes them visible in page source and devtools.


The core rule is simple: if a browser can use a secret directly, a user can extract it. So don’t put privileged API keys in the browser unless the system is explicitly designed for that model.


Draw the trust boundary first


For a realtime avatar app, there are three distinct things people often conflate:


  1. The long-lived API key used to manage avatars, sessions, and billing.

  2. A short-lived session credential or token used by a specific browser session.

  3. The media connection itself — usually WebRTC or a similar streaming transport, which is negotiated after authentication.


The API key belongs on the server. The browser can receive a scoped, short-lived credential if the system supports it, but not the secret that can create arbitrary sessions or read account data.


In Next.js terms, that means:


  • Use server routes, server actions, or backend services for anything that touches the privileged API.

  • Keep client components limited to UI, local state, and calls to your own app endpoints.

  • Never embed the production API key in a client component, runtime config exposed to the browser, or a public env var.


A safer session-creation flow


The easiest secure pattern is to have the browser request a session from your app, and have your app create or authorize that session server-side.


In Next.js, the browser might POST user intent to a route handler:


export async function POST(req: Request) {

}
export async function POST(req: Request) {

}
export async function POST(req: Request) {

}


The browser then uses the response from your app, not your API key. If the session response includes a short-lived token, websocket URL, or other connection metadata, that can be sent to the frontend because it is already scoped to a single session.


If you need the frontend to control session lifecycle, treat the browser as an untrusted client and validate everything server-side: user identity, allowed avatar IDs, voice settings, duration, and rate limits.


Next.js-specific pitfalls


Next.js gives you enough rope to make this easy to get wrong.


1. Watch your env var prefixes. Anything with NEXT_PUBLIC_ is public by design. That is fine for non-secrets like feature flags or public IDs. It is not fine for API keys.


2. Keep secret-bearing code in server-only modules. If a file imports client-only code or is used by a client component, assume it can be bundled or serialized in ways you do not want. A good practice is to isolate privileged calls into route handlers or server utilities that are never imported from client components.


3. Don’t proxy secrets through client parameters. It’s tempting to pass an API key into a React hook “just for development.” That habit tends to survive into production. If the browser needs to authenticate, exchange a user session for a scoped token on the server.


4. Be careful with source maps and logs. Even if the key is not in the bundle, avoid logging headers, failed requests, or full serialized responses from privileged calls. The easiest leak is still the one you printed for debugging.


How to structure the frontend


On the React side, keep the component tree dumb about secrets. A clean pattern is:


  • a client component gathers user input and shows connection state;

  • it calls your own endpoint to create or refresh a session;

  • it initializes the realtime avatar transport using only the returned scoped session data.


That same pattern works whether the avatar is rendered inline, attached to a voice agent, or embedded in a larger app. The frontend should know how to display and connect, not how to authenticate against your management API.


For example, a client component might do this:


async function startAvatar() {

}
async function startAvatar() {

}
async function startAvatar() {

}


When a realtime avatar needs a backend anyway


If you are building a voice agent or a customer-support bot, you almost always need a backend even if the UI feels “frontend-heavy.” The backend may:


  • create a realtime session;

  • attach the avatar to a live conversation;

  • enforce per-user quotas;

  • translate app auth into Protoface access;

  • log usage for billing or abuse detection.


That backend is also where you can centralize rotation of your API key, key scoping by environment, and operational controls like request throttling. In practice, this is the difference between a demo and a production integration.


Protoface-specific pattern: keep the key server-side


Protoface is designed around this split. For server-side integrations, use the REST API or a backend SDK to create and manage avatars and realtime sessions, and keep the API key in your server environment. For voice-agent integrations, the LiveKit plugin follows the same principle: your agent backend owns the integration, and the browser never needs the secret.


A minimal server-side call looks like this:


curl https://api.protoface.com/v1/sessions \
-d '{"avatar_id":"avt_123"}'
curl https://api.protoface.com/v1/sessions \
-d '{"avatar_id":"avt_123"}'
curl https://api.protoface.com/v1/sessions \
-d '{"avatar_id":"avt_123"}'


If you are using Python, the same idea applies with the SDK: read the key from the environment on the server, create the session there, then return only the data the client actually needs. See the docs at docs.protoface.com for the exact request and response shapes.


For embedded web experiences where you do not want to operate a backend at all, the customer-managed iframe model is the safer answer because no API key ever reaches the browser. That’s not a workaround; it’s the intended architecture when the use case fits an embed.


Operational habits that prevent leakage


A few practices go a long way:


  • Rotate keys regularly and revoke immediately if they appear in logs or tickets.

  • Use separate keys per environment so dev mistakes do not affect production usage.

  • Store secrets only server-side in environment variables or a secret manager.

  • Audit your bundles for accidental literals, especially after adding analytics or debug tooling.

  • Limit blast radius with rate limits, short-lived session credentials, and per-user authorization checks.


For Next.js apps, also inspect the final deployed bundle and server logs. If a key appears anywhere other than the server process, treat it as compromised.


Conclusion


Preventing API key leakage in React and Next.js is mostly about respecting the browser boundary. Keep long-lived API keys on the server, exchange them for scoped session data when needed, and make the frontend operate only on short-lived, user-specific credentials. For realtime avatar apps, that separation matters even more because the integration often spans UI, WebRTC-style media negotiation, and backend session management.


If you are wiring this up now, start with your server route, not your client component. Keep the master key out of the bundle, validate all session creation server-side, and use the appropriate integration path for your architecture. The docs at docs.protoface.com cover the API and supported surfaces in more detail.

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.