LiveKit docs › Building Frontends › Session management

---

# Session management

> Use Session APIs to manage room connections and agent lifecycle in your frontend.

## Overview

Building an agent frontend requires coordinating several moving parts: fetching authentication tokens, connecting to a room, dispatching an agent, and then interacting with it through media, data, and state APIs. The `Session` API handles this orchestration for you, giving you a single entry point that manages the full lifecycle of a 1:1 agent interaction.

Without `Session`, you need to generate tokens manually, call `Room.connect`, set up agent dispatch configuration, and manage cleanup yourself. `Session` wraps all of this into a simple start/end interface while still giving you access to lower-level APIs when needed.

`Session` is available on Web, Swift, Kotlin, Flutter, and React Native.

> ℹ️ **Note**
> 
> If you're using a platform that doesn't yet support Session APIs, you can use manual token generation and connect directly with `Room.connect`. See [Token creation](https://docs.livekit.io/frontends/reference/tokens-grants.md) for details.

## Session lifecycle

A session has four stages:

1. **Create**: Initialize a session with a `TokenSource` and options including the agent name. `TokenSource` handles token fetching, caching, and refreshing — see the [Authentication](https://docs.livekit.io/frontends/build/authentication.md) guide for setup.
2. **Start**: Call `session.start()` to fetch a token, connect to a room, and dispatch the agent.
3. **Interact**: The agent joins the room and begins the conversation. Your frontend can access agent state, media tracks, transcriptions, and data through the session.
4. **End**: Call `session.end()` to disconnect from the room and clean up resources.

## What you get inside a session

Once a session is started and the agent has joined, your frontend has access to the full set of realtime APIs for interacting with the agent.

### Agent state

The agent moves through a lifecycle of states — connecting, listening, thinking, speaking — that your frontend can observe. Use these states to drive UI updates, like showing a visual indicator when the agent is thinking or disabling the microphone when the agent is speaking. State getters like `canListen` and `isFinished` simplify common UI decisions. See [Agent state](https://docs.livekit.io/frontends/build/agent-state.md) for details.

### Media tracks

Your frontend and agent exchange audio and video over media tracks. A simple voice agent subscribes to the user's microphone and publishes its own audio. Agents with vision capabilities can subscribe to the user's camera or screen share. See [Realtime media and data](https://docs.livekit.io/frontends/build/media-data.md) for details.

### Session messages

The session exposes a unified list of messages you can use to build chat panels and message lists. Your SDK provides a Session Messages API that gives you the list and a way to send new chat messages.

The following examples show how to render the message list and send user messages on each platform.

**React**:

```tsx
'use client';

import { useState } from 'react';
import { useAgent, useSessionMessages } from '@livekit/components-react';
import { AgentChatTranscript } from '@/components/agents-ui/agent-chat-transcript';

function ChatInterface({ session }) {
  const { state } = useAgent(session);
  const { messages, send, isSending } = useSessionMessages(session);
  const [chatMessage, setChatMessage] = useState('');

  return (
    <>
      <AgentChatTranscript agentState={state} messages={messages} />
      <div>
        <input
          type="text"
          value={chatMessage}
          onChange={(e) => setChatMessage(e.target.value)}
        />
        <button
          disabled={isSending}
          onClick={() => {
            send(chatMessage);
            setChatMessage('');
          }}
        >
          {isSending ? 'Sending' : 'Send'}
        </button>
      </div>
    </>
  );
}

```

---

**Swift**:

```swift
// In a SwiftUI view with access to your session:
ForEach(session.messages) { message in
  switch message.content {
  case let .agentTranscript(text),
       let .userTranscript(text),
       let .userInput(text):
    Text(text)
  }
}

TextField("Message", text: $inputText)
Button("Send") {
  Task {
    await session.send(text: inputText)
    inputText = ""
  }
}

```

---

**Android**:

```kotlin
val sessionMessages = rememberSessionMessages()

LazyColumn {
  items(items = sessionMessages.messages) { message ->
    Text(message.message)
  }
}

val messageState = rememberTextFieldState()
TextField(state = messageState)
Button(onClick = {
  coroutineScope.launch {
    sessionMessages.send(messageState.text.toString())
    messageState.clearText()
  }
}) {
  Text("Send")
}

```

---

**Flutter**:

```dart
// session.messages is the list; session.sendText() sends user messages
ListView.builder(
  itemCount: session.messages.length,
  itemBuilder: (context, index) {
    final message = session.messages[index];
    return Text(message.content.text);
  },
)

TextField(
  controller: _controller,
  onSubmitted: (text) async {
    await session.sendText(text);
    _controller.clear();
  },
)

```

#### Chat messages

Messages that the user or agent types (rather than speaks) appear in the session messages list. Use the Session Messages API's `send()` method to post new messages from the user. The agent can add messages on its side via the same channel.

#### Transcriptions

Transcriptions of agent and user speech are included in the session messages list. They also exist as raw text streams for captions or custom UIs. See [Text streams](https://docs.livekit.io/transport/data/text-streams.md) for details.

### Data and state synchronization

Beyond media and transcriptions, your frontend and agent can exchange arbitrary data. Use byte streams for files and images, RPC for request-response interactions, and state synchronization for shared key-value data that stays in sync across participants. See [Realtime media and data](https://docs.livekit.io/frontends/build/media-data.md#data) for an overview.

## Session lifecycle examples

The following examples show how to create a session, start it (connect and dispatch the agent), and end it on each platform.

**React**:

```tsx
'use client';
import { useEffect } from 'react';
import { useSession, SessionProvider } from '@livekit/components-react';
import { TokenSource } from 'livekit-client';

const tokenSource = TokenSource.sandboxTokenServer('%{firstSandboxTokenServerName}%');

export function App() {
  const session = useSession(tokenSource, { agentName: 'my-agent' });

  useEffect(() => {
    session.start();
    return () => {
      session.end();
    };
  }, []);

  return (
    <SessionProvider session={session}>
      {/* Your app components */}
    </SessionProvider>
  );
}

```

---

**Swift**:

```swift
import LiveKitComponents

let tokenSource = SandboxTokenSource(id: "%{firstSandboxTokenServerName}%")
let session = Session.withAgent("my-agent", tokenSource: tokenSource)

// Start the session
await session.start()

// End the session
await session.end()

```

---

**Android**:

```kotlin
val tokenSource = remember {
    TokenSource.fromSandboxTokenServer("%{firstSandboxTokenServerName}%")
}
val session = rememberSession(tokenSource = tokenSource)

LaunchedEffect(Unit) {
    session.start()
}

// End the session when done
session.end()

```

---

**Flutter**:

```dart
import 'package:livekit_client/livekit_client.dart' as sdk;

final tokenSource = sdk.SandboxTokenSource(sandboxId: "%{firstSandboxTokenServerName}%");
final session = sdk.Session.fromConfigurableTokenSource(
  tokenSource,
  const TokenRequestOptions(agentName: "my-agent"),
);

await session.start();

// End the session when done
await session.end();

```

## End-to-end encryption

Use E2EE when content needs to stay fully encrypted from sender to receiver so that no intermediaries (including LiveKit servers) can access or modify it. Sessions can be configured with end-to-end encryption at creation time, where both media tracks and data channels are encrypted with keys distributed by you that the server never sees. See the [Encryption overview](https://docs.livekit.io/transport/encryption.md) for details.

**React**:

```tsx
'use client';
import { useEffect, useState } from 'react';
import { useSession, SessionProvider } from '@livekit/components-react';
import { TokenSource } from 'livekit-client';

const tokenSource = TokenSource.sandboxTokenServer('%{firstSandboxTokenServerName}%');

export function App() {
  const [worker] = useState(() => {
    if (typeof window === 'undefined') {
      return null;
    }
    return new Worker(new URL('livekit-client/e2ee-worker', import.meta.url));
  });

  const session = useSession(tokenSource, {
    agentName: 'my-agent',
    encryption: worker ? { key: 'your-shared-key', worker } : undefined,
  });

  useEffect(() => {
    session.start();
    return () => {
      session.end();
    };
  }, []);

  return (
    <SessionProvider session={session}>
      {/* Your app components */}
    </SessionProvider>
  );
}

```

---

**Swift**:

```swift
import LiveKitComponents

let tokenSource = SandboxTokenSource(id: "%{firstSandboxTokenServerName}%")
let session = Session.withAgent(
    "my-agent",
    tokenSource: tokenSource,
    options: SessionOptions(encryption: .sharedKey("your-shared-key"))
)

await session.start()

```

---

**Android**:

```kotlin
val tokenSource = remember {
    TokenSource.fromSandboxTokenServer("%{firstSandboxTokenServerName}%")
}
val session = rememberSession(
    tokenSource = tokenSource,
    options = SessionOptions(encryption = E2EEOptions(sharedKey = "your-shared-key")),
)

LaunchedEffect(Unit) {
    session.start()
}

```

---

**Flutter**:

```dart
import 'package:livekit_client/livekit_client.dart' as sdk;

final tokenSource = sdk.SandboxTokenSource(sandboxId: "%{firstSandboxTokenServerName}%");
final session = sdk.Session.withAgent(
  "my-agent",
  tokenSource: tokenSource,
  options: sdk.SessionOptions(
    encryption: await sdk.E2EEOptions.sharedKey("your-shared-key"),
  ),
);

await session.start();

```

---

**React Native**:

```tsx
import { useEffect } from 'react';
import { useSession, SessionProvider } from '@livekit/components-react';
import { useRNE2EEManager } from '@livekit/react-native';
import { TokenSource } from 'livekit-client';

const tokenSource = TokenSource.sandboxTokenServer('%{firstSandboxTokenServerName}%');

export function App() {
  const { e2eeManager } = useRNE2EEManager({ sharedKey: 'your-shared-key' });

  const session = useSession(tokenSource, {
    agentName: 'my-agent',
    encryption: { e2eeManager: e2eeManager },
  });

  useEffect(() => {
    session.start();
    return () => {
      session.end();
    };
  }, []);

  return (
    <SessionProvider session={session}>
      {/* Your app components */}
    </SessionProvider>
  );
}

```

### Toggling encryption at runtime

Call `setEncryptionEnabled` on the session to turn E2EE on or off after the session is started. A common use is downgrading an encrypted session to an unencrypted one when a participant joins without encryption support, so the rest of the room can still communicate with them. Encryption must be configured via the `encryption` option at session creation.

**React**:

```tsx
await session.setEncryptionEnabled(false);

```

---

**Swift**:

```swift
session.setEncryptionEnabled(false)

```

---

**Android**:

```kotlin
session.setEncryptionEnabled(false)

```

---

**Flutter**:

```dart
await session.setEncryptionEnabled(false);

```

---

**React Native**:

```tsx
await session.setEncryptionEnabled(false);

```

> ℹ️ **Info**
> 
> The `encryption` option on `SessionOptions` covers the shared-key case. For custom key providers (per-participant keys, MLS/MEGOLM-style rotation, etc.), pass a pre-built `Room` configured with your own key provider — see [Using a custom key provider](https://docs.livekit.io/transport/encryption/start.md#custom-key-provider) for more information.

## Using AgentSessionProvider

If you're building with [Agents UI](https://docs.livekit.io/frontends/agents-ui.md) components, the `AgentSessionProvider` component wraps the session from `useSession` and provides session context to all child components. Agents UI components like `AgentControlBar`, `AgentChatTranscript`, and the audio visualizers require this provider as an ancestor.

```tsx
'use client';
import { useSession } from '@livekit/components-react';
import { AgentSessionProvider } from '@/components/agents-ui/agent-session-provider';
import { TokenSource } from 'livekit-client';

const tokenSource = TokenSource.sandboxTokenServer('%{firstSandboxTokenServerName}%');

export function App() {
  const session = useSession(tokenSource, { agentName: 'my-agent' });

  return (
    <AgentSessionProvider session={session}>
      {/* Agents UI components can access session context here */}
    </AgentSessionProvider>
  );
}

```

- **[AgentSessionProvider reference](https://docs.livekit.io/reference/components/agents-ui/component/agent-session-provider.md)**: Full API reference and props documentation.

## Next steps

- **[Authentication](https://docs.livekit.io/frontends/build/authentication.md)**: Configure token generation for development and production.

- **[Agent state](https://docs.livekit.io/frontends/build/agent-state.md)**: Track agent lifecycle states to drive your UI.

- **[Realtime media and data](https://docs.livekit.io/frontends/build/media-data.md)**: Work with audio, video, transcriptions, and data inside a session.

- **[UI components](https://docs.livekit.io/frontends/agents-ui.md)**: Add pre-built components for session management, media, and chat.

---

This document was rendered at 2026-06-07T11:34:11.629Z.
For the latest version of this document, see [https://docs.livekit.io/frontends/build/sessions.md](https://docs.livekit.io/frontends/build/sessions.md).

To explore all LiveKit documentation, see [llms.txt](https://docs.livekit.io/llms.txt).