Adding User-Selectable Avatar Presets in a Next.js App with the OpenAI Realtime API

Next.js avatar presets with OpenAI Realtime: server-side session creation, preset persistence, and secure reconnects.
Introduction
Adding user-selectable avatar presets sounds trivial until you wire it into a realtime avatar pipeline. The UI problem is simple: let a user pick from a small set of faces, then remember that choice. The systems problem is harder: in a streaming conversation, the selected preset has to flow into session creation, stay consistent across reconnects, and not leak any credentials into the browser.
This post shows one practical way to do that in a Next.js app using the OpenAI Realtime API for the conversation layer and an avatar service for the video face. By the end, you should be able to:
model avatar presets cleanly in your app
create a realtime session from the server, not the browser
start a streaming conversation with a user-selected avatar
avoid the common auth and state-management mistakes that break these flows
What “avatar preset” should mean in a realtime app
An avatar preset should be an immutable product choice, not a free-form blob of UI state. In practice, it’s usually a small object with a stable identifier and the fields needed to render and configure the avatar session.
For example:
That structure matters because the same preset needs to be used in at least three places:
the selection UI
your server-side session bootstrap
the persisted conversation state, so reconnects restore the same face
The important design constraint is that the browser should only ever send a preset ID. The server resolves that ID to the real avatar configuration and any upstream session parameters. If you pass raw avatar IDs or provider secrets into client code, you’ll eventually ship a credential leak.
Keep the browser dumb: let Next.js own session creation
The clean pattern in Next.js is to let the client choose a preset, then call a server route that creates the realtime session and returns only the data the browser needs to connect. That route can also validate that the preset is allowed for the current user or tenant.
In a typical app, the flow looks like this:
User selects a preset in the React UI.
Client posts
presetIdto/api/realtime/session.Server maps
presetIdto the underlying avatar config.Server creates the session with your upstream realtime provider and avatar service.
Server returns a short-lived token or session descriptor to the browser.
That architecture gives you a few things for free:
API keys stay server-side.
You can enforce tenant-specific preset availability.
You can swap avatar implementations without changing the UI.
You can audit and rate-limit session creation centrally.
Client-side selection in Next.js
The UI itself can be plain React. Keep the preset list local to the app or fetch it from your own backend. The key is that the selected preset becomes an input to session creation, not the session itself.
Two details are worth calling out:
The browser never needs to know your avatar provider API key.
The client should not decide the avatar’s real backend identity; it should only choose a preset.
Server route: resolve the preset and create the session
Your server route is where preset IDs become real session configuration. If you are using OpenAI Realtime for the conversation and an avatar service for the face, this is where you bind them together.
A minimal example in a Next.js route handler might look like this:
In production, this route should also:
authenticate the user
verify that the preset is allowed for that account
log the preset ID for usage analytics
apply expiry limits to the returned session token
If you need to create or manage avatar resources programmatically, use the server-side API rather than a browser call. The REST API is authenticated with API keys and is intended for this kind of backend orchestration. For the exact request shapes, refer to the docs.
How the streaming pieces fit together
Realtime avatars are not “video files with chat.” They are streaming endpoints that synthesize a talking face from the current conversational state, usually over WebRTC or a similar low-latency transport. The avatar needs text or speech input quickly enough to stay synchronized with the model’s response. That means latency is dominated by the chain of events, not any single API call:
user audio is captured
speech is transcribed or directly routed into the conversation model
the model produces a response incrementally
the avatar stream renders mouth movement and facial motion from that response
When you add selectable presets, you are really choosing a bundle of parameters that affects the entire chain: which avatar to render, which voice to use, and what instructions shape the model’s behavior. If those parameters diverge between the client and the avatar session, you get an obvious mismatch: the UI says “friendly support,” but the session sounds like a product demo.
A few practical gotchas:
Don’t recreate sessions on every render. Treat the selected preset as stable state until the user changes it.
Keep reconnect logic idempotent. Rejoining should reuse the same preset ID and restore the same server-side session or create an equivalent one.
Use short-lived tokens if your browser connects directly to a realtime transport.
Separate preset metadata from generated session state. They are not the same thing.
Using Protoface for the avatar side of the stack
This is a good fit for Protoface because the avatar/session boundary is already exposed as a backend service. In this architecture, your Next.js app stays responsible for preset selection and auth, while the server creates the avatar session and hands the browser only a short-lived connection payload.
That maps well to the REST API and the Python SDK if you want to automate avatar creation or session management on the backend. For example, a backend service can create a session from a preset and return a browser-safe descriptor:
If you are using a voice agent stack, the LiveKit integration also makes this pattern straightforward: drop the avatar into the agent so the voice output and facial animation stay synchronized. The plugin is published as livekit-plugins-protoface on PyPI, and the examples in the repo are the fastest way to see how the handoff works in practice.
Testing, persistence, and production concerns
Once the basic flow works, the last mile is mostly about state and failure modes.
Persist the selected preset on the user profile if the choice should survive reloads. If the preset is session-scoped, keep it in route state or a conversation record instead. Either way, the server should treat the preset ID as the source of truth when generating the session.
Also verify these cases before shipping:
invalid preset IDs return a 400, not a default avatar
unauthorized users cannot select premium presets
session reconnects preserve the original preset
rate limits prevent one user from creating many avatar sessions in a loop
If you expose the avatar surface to untrusted clients, prefer an iframe-based embed model for isolation. If you are building a first-party app with your own auth and app shell, the Next.js route-handler approach is usually simpler and gives you more control.
Conclusion
User-selectable avatar presets are mostly a session-management problem disguised as a UI feature. Keep the preset list small and explicit, let the browser send only a preset ID, resolve that ID on the server, and create the realtime session there. That keeps credentials off the client, makes reconnects predictable, and gives you a single place to enforce product rules.
If you want to implement this with Protoface, start by wiring your Next.js backend to create avatar sessions from preset IDs, then connect the browser to the returned realtime session data. The public docs at docs.protoface.com cover the API details, and the GitHub quickstarts are a good way to compare integration styles before you commit to one.
