Chapter 220m

Connecting to the dental receptionist

Connecting to the dental receptionist

Time to scaffold the project and hear Maya greet you from the browser. In this chapter, you will create a Next.js app using the LiveKit agent starter template, build a token endpoint that authenticates patients, and connect to your dental receptionist agent via the Session API.

AccessTokenGrantsSession APIToken server

What you'll learn

  • How to scaffold a LiveKit agent frontend with the starter template
  • How LiveKit JWT tokens work and what claims they contain
  • How to build a token endpoint that grants patients access to a dental session room
  • How grants control what a patient can do (publish microphone audio, subscribe to agent audio, send text)
  • How to connect to Maya using the Session API

Scaffold the project

The fastest way to start is the LiveKit agent starter template for React. It gives you a working Next.js project with the LiveKit client SDK, React components, and a sandbox token source pre-configured.

Terminalbash
npx create-livekit-app@latest dental-frontend --template agent-starter-react
cd dental-frontend
pnpm install

The generated project includes:

FilePurpose
app/page.tsxMain page with useSession and agent UI components
app/api/token/route.tsToken endpoint (you will customize this)
.env.localLiveKit credentials
components/Pre-built agent UI components

Start with the sandbox

The template ships with a sandbox token source that connects to a LiveKit sandbox environment. Run pnpm dev and you should be able to talk to a default agent immediately. You will replace this with your dental receptionist shortly.

How LiveKit tokens work

A LiveKit token is a standard JSON Web Token (JWT) signed with your API secret. When the patient's browser wants to join a room, it presents this token to LiveKit Cloud. The server verifies the signature, reads the claims, and either allows or rejects the connection.

The token contains three key pieces of information:

ClaimPurposeDental example
IdentityUnique ID for the participant"patient-jane-doe"
RoomWhich room this token grants access to"dental-session-a1b2c3"
GrantsWhat the participant is allowed to doJoin, publish audio, subscribe, send data

Tokens are short-lived — typically 10 minutes. That is long enough to establish a connection but short enough to limit damage if one leaks.

What's happening

The token is the only thing standing between an anonymous browser and your dental receptionist's room. It replaces traditional session cookies for the real-time connection. Because the token is signed with your API secret (which never leaves your server), LiveKit Cloud trusts that the bearer was authorized by your application.

Build the dental token endpoint

Replace the scaffolded token endpoint with one tailored to the dental receptionist. It generates a room name for each session, assigns a patient identity, and grants the permissions needed for a voice AI conversation with text input.

app/api/token/route.tstypescript
import { AccessToken } from "livekit-server-sdk";
import { NextRequest, NextResponse } from "next/server";
import { randomUUID } from "crypto";

export async function GET(request: NextRequest) {
const participantName =
  request.nextUrl.searchParams.get("participantName") ?? "patient";

const apiKey = process.env.LIVEKIT_API_KEY;
const apiSecret = process.env.LIVEKIT_API_SECRET;

if (!apiKey || !apiSecret) {
  return NextResponse.json(
    { error: "Server misconfigured — missing LiveKit credentials" },
    { status: 500 }
  );
}

// Each dental conversation gets its own room
const roomName = `dental-session-${randomUUID().slice(0, 8)}`;

const at = new AccessToken(apiKey, apiSecret, {
  identity: `patient-${participantName}-${randomUUID().slice(0, 4)}`,
  ttl: "10m",
});

at.addGrant({
  roomJoin: true,
  room: roomName,
  canPublish: true,      // Microphone audio
  canSubscribe: true,    // Agent's TTS audio + text streams
  canPublishData: true,  // Text input messages
});

const token = await at.toJwt();

return NextResponse.json({ token });
}

Never expose your API secret

The LIVEKIT_API_KEY and LIVEKIT_API_SECRET variables have no NEXT_PUBLIC_ prefix — intentionally. Next.js only exposes NEXT_PUBLIC_-prefixed variables to the browser. Your API secret stays on the server.

Understanding grants for a dental session

The addGrant call defines exactly what the patient can do inside the room. Here is every grant relevant to the dental receptionist frontend:

GrantValueWhy the dental frontend needs it
roomJointruePatient must be able to join the room
room"dental-session-..."Scoped to this specific session
canPublishtruePatient needs to publish microphone audio so the agent can hear them
canSubscribetruePatient needs to receive the agent's TTS audio, text streams, and attributes
canPublishDatatruePatient needs to send text messages (typing names, insurance IDs)
What's happening

Grants follow the principle of least privilege. A supervisor monitoring a call would get canSubscribe: true but canPublish: false. A patient in the dental receptionist app needs bidirectional audio and data — they speak, they listen, and they type.

Set up environment variables

Add your LiveKit Cloud credentials from the dashboard (Settings > Keys):

.env.localbash
LIVEKIT_API_KEY=your-api-key
LIVEKIT_API_SECRET=your-api-secret
LIVEKIT_URL=wss://your-project.livekit.cloud

These are the same credentials your dental receptionist agent uses. The frontend and agent connect to the same LiveKit Cloud project.

Connect to the dental receptionist with the Session API

Open app/page.tsx and replace the sandbox token source with your custom endpoint. The Session API will call your /api/token route automatically.

app/page.tsxtsx
"use client";

import { useSession } from "@livekit/components-react";
import { TokenSource } from "livekit-client";

export default function DentalReceptionist() {
const session = useSession({
  tokenSource: TokenSource.endpoint({
    url: "/api/token",
  }),
});

return (
  <main className="flex min-h-screen flex-col items-center justify-center bg-white">
    <h1 className="text-2xl font-semibold text-gray-900">
      Bright Smile Dental
    </h1>
    <p className="mt-2 text-gray-500">
      {session.agent.agentState === "connecting"
        ? "Connecting to Maya..."
        : session.agent.agentState === "disconnected"
          ? "Session ended"
          : "Speak to our receptionist"}
    </p>
  </main>
);
}

When useSession runs, here is the flow:

1

Token request

The Session API calls GET /api/token?participantName=patient to fetch a JWT.

2

Room connection

With the token, the Session API connects to LiveKit Cloud via WebRTC. The SFU verifies the signature and grants entry.

3

Agent dispatch

LiveKit Cloud dispatches your dental receptionist agent into the room. Maya connects as an AGENT participant.

4

Maya greets the patient

The agent's on_enter fires, calling session.say("Thanks for calling Bright Smile Dental! How can I help you today?"). The patient hears Maya's greeting through the browser.

Test it

Terminalbash
pnpm dev

Open http://localhost:3000. You should hear Maya greet you. If your dental receptionist agent is deployed on LiveKit Cloud, it connects automatically. If you are running the agent locally with lk agent dev, make sure it is running in a separate terminal.

Verify the token endpoint independently:

Terminalbash
curl "http://localhost:3000/api/token?participantName=test-patient"

You should see a JSON response with a JWT:

Responsejson
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Development vs production token sources

During development, you can keep the sandbox as a fallback:

app/page.tsx (conditional token source)typescript
const tokenSource = process.env.NEXT_PUBLIC_LIVEKIT_SANDBOX_ID
? TokenSource.sandbox({
    sandboxId: process.env.NEXT_PUBLIC_LIVEKIT_SANDBOX_ID,
  })
: TokenSource.endpoint({ url: "/api/token" });

Remove the sandbox ID from your environment when you deploy.

What you learned

  • The LiveKit agent starter template scaffolds a working Next.js project with client SDK and React components
  • LiveKit tokens are JWTs containing identity, room, and grant claims, signed with your API secret
  • The dental token endpoint generates a unique room per session and grants publish, subscribe, and data permissions
  • TokenSource.endpoint tells the Session API to fetch tokens from your custom route
  • The Session API handles the full lifecycle — token fetch, room connection, agent dispatch, reconnection

Test your knowledge

Question 1 of 2

Why does the dental token endpoint generate a unique room name for each session?

Next up

Your token endpoint generates JWTs, but it does not yet control which agent joins the room. In the next chapter, you will learn about agent dispatch — automatic vs. explicit — and upgrade your token to dispatch the dental receptionist by name.

Concepts covered
AccessTokenGrantsSession APIToken server