Docs

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 authorize is really gating.
  • Calls — everything the session policy can decide.