TanStack Start
Mount the realtime avatar proxy in a TanStack Start app: a splat server route 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 Start that is one server route.
The server half
Mount at routes/api/realtime-avatar/$.ts. The trailing $ is Start's splat segment and it is load-bearing: without it the handler only ever sees the mount path itself and answers 404 for every operation — the same trap Next.js's [...path] exists to avoid.
// routes/api/realtime-avatar/$.ts
import { createFileRoute } from "@tanstack/react-router";
import { realtimeAvatarServerRoute } from "realtime-avatar/tanstack-start";
export const Route = createFileRoute("/api/realtime-avatar/$")({
server: {
handlers: realtimeAvatarServerRoute({
apiKey: process.env.REALTIME_AVATAR_API_KEY!,
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 });
}
},
session: async ({ avatarId }) => ({
instructions: promptFor(avatarId),
}),
}),
},
});On Cloudflare Workers there is no process.env, so pass apiKey as a factory — apiKey: () => getEnv().REALTIME_AVATAR_API_KEY — and it is read per request instead of at module scope.
The client half
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} />;
}Next
- Authentication — keys, scopes, and what
authorizeis really gating. - Calls — everything the
sessionpolicy can decide.