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.
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.
npx create-livekit-app@latest dental-frontend --template agent-starter-react
cd dental-frontend
pnpm installThe generated project includes:
| File | Purpose |
|---|---|
app/page.tsx | Main page with useSession and agent UI components |
app/api/token/route.ts | Token endpoint (you will customize this) |
.env.local | LiveKit 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:
| Claim | Purpose | Dental example |
|---|---|---|
| Identity | Unique ID for the participant | "patient-jane-doe" |
| Room | Which room this token grants access to | "dental-session-a1b2c3" |
| Grants | What the participant is allowed to do | Join, 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.
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.
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:
| Grant | Value | Why the dental frontend needs it |
|---|---|---|
roomJoin | true | Patient must be able to join the room |
room | "dental-session-..." | Scoped to this specific session |
canPublish | true | Patient needs to publish microphone audio so the agent can hear them |
canSubscribe | true | Patient needs to receive the agent's TTS audio, text streams, and attributes |
canPublishData | true | Patient needs to send text messages (typing names, insurance IDs) |
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):
LIVEKIT_API_KEY=your-api-key
LIVEKIT_API_SECRET=your-api-secret
LIVEKIT_URL=wss://your-project.livekit.cloudThese 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.
"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:
Token request
The Session API calls GET /api/token?participantName=patient to fetch a JWT.
Room connection
With the token, the Session API connects to LiveKit Cloud via WebRTC. The SFU verifies the signature and grants entry.
Agent dispatch
LiveKit Cloud dispatches your dental receptionist agent into the room. Maya connects as an AGENT participant.
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
pnpm devOpen 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:
curl "http://localhost:3000/api/token?participantName=test-patient"You should see a JSON response with a JWT:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Development vs production token sources
During development, you can keep the sandbox as a fallback:
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.endpointtells 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.