Docs

Next.js

Mount the realtime avatar proxy in a Next.js App Router project: one route file on the server, one component in the browser.

Your API key must never reach a browser, so the browser talks to your app and your app talks to us. In Next.js that is one file.

The server half

App Router, mounted at a catch-all so every operation reaches the handler. The [...path] segment is not decoration — without it the route only ever matches the mount path itself and answers 404 for connect, end, avatars and credits alike.

// app/api/realtime-avatar/[...path]/route.ts
import { createRealtimeAvatarRoute } from "realtime-avatar/nextjs";

export const { GET, POST } = createRealtimeAvatarRoute({
  apiKey: process.env.REALTIME_AVATAR_API_KEY!,

  // Who may do this. Return a Response to refuse, or nothing to allow.
  // "connect" is the only operation that costs money to start, so that is
  // where the wallet check belongs; the reads stay cheap.
  authorize: async ({ request, operation }) => {
    const user = await currentUser(request);
    if (!user) return new Response("Sign in", { status: 401 });
    if (operation === "connect" && !user.credits) {
      return Response.json({ code: "insufficient_credits" }, { status: 402 });
    }
  },

  // What the character knows. Decided HERE — the client sends none of it.
  session: async ({ avatarId }) => ({
    instructions: promptFor(avatarId),
  }),
});

Name the variable without NEXT_PUBLIC_. That prefix is what inlines a value into the client bundle, so a key wearing it ships to every visitor — the one mistake this whole split exists to prevent.

The client half

One component, pointed at the route you just mounted. It is the same component on every framework — see React for the hook underneath it and for controlling the call yourself.

"use client";
import { AvatarCall, createProxyClient } from "realtime-avatar/react";

const client = createProxyClient({ proxyUrl: "/api/realtime-avatar" });

export function Call({ avatarId }: { avatarId: string }) {
  return <AvatarCall client={client} avatarId={avatarId} />;
}

"use client" is required: the call holds a WebRTC room and browser media, none of which exist in a server component.

Next

  • Authentication — keys, scopes, and what authorize is really gating.
  • Calls — everything the session policy can decide.