---
title: Server Controlled Example
url: https://docs.kaltura.com/docs/conversational-agents/agentic-avatars-sdk/examples-server-controlled
---

## Server Controlled Example

Production-ready example with backend API control and frontend display.

> **Recommended for Production**
> This architecture keeps your Kaltura Session secure and provides better separation of concerns.

### Architecture

```
Frontend (React)  <->  Backend (Express)  <->  Kaltura API
  Display only       Control + Security      Sessions
```

### Backend (Express + Node.js)

#### `server.js`

```javascript
import express from 'express';
import fetch from 'node-fetch';
import cors from 'cors';
import dotenv from 'dotenv';

dotenv.config();

const app = express();
app.use(cors());
app.use(express.json());

const KS = process.env.AVATAR_KS;
const BASE_URL = process.env.AVATAR_BASE_URL || 'https://api.avatar.us.kaltura.ai/v1/avatar-session';

// Session storage (use Redis/database in production)
const sessions = new Map();

// Create session
app.post('/api/avatar/create-session', async (req, res) => {
  try {
    const { visualId, voiceId } = req.body;

    const response = await fetch(BASE_URL, {
      method: 'POST',
      headers: {
        Authorization: `KS $\{KS\}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        visualConfig: { id: visualId },
        voiceConfig: voiceId ? { id: voiceId } : undefined,
      }),
    });

    if (!response.ok) {
      throw new Error(`API error: ${response.status}`);
    }

    const data = await response.json();

    sessions.set(data.sessionId, {
      token: data.token,
      createdAt: new Date(),
      visualId,
      voiceId,
    });

    res.json({
      sessionId: data.sessionId,
      token: data.token,
    });
  } catch (error) {
    console.error('Create session error:', error);
    res.status(500).json({ error: 'Failed to create session' });
  }
});

// Say text
app.post('/api/avatar/say-text', async (req, res) => {
  try {
    const { sessionId, token, text } = req.body;

    if (!sessions.has(sessionId)) {
      return res.status(404).json({ error: 'Session not found' });
    }

    if (!text || text.trim().length === 0) {
      return res.status(400).json({ error: 'Text is required' });
    }

    const response = await fetch(`$\{BASE_URL\}/${sessionId}/say-text`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ text: text.trim() }),
    });

    if (!response.ok) {
      throw new Error(`API error: ${response.status}`);
    }

    const data = await response.json();
    res.json(data);
  } catch (error) {
    console.error('Say text error:', error);
    res.status(500).json({ error: 'Failed to say text' });
  }
});

// Interrupt
app.post('/api/avatar/interrupt', async (req, res) => {
  try {
    const { sessionId, token } = req.body;

    if (!sessions.has(sessionId)) {
      return res.status(404).json({ error: 'Session not found' });
    }

    const response = await fetch(`$\{BASE_URL\}/${sessionId}/interrupt`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });

    if (!response.ok) {
      throw new Error(`API error: ${response.status}`);
    }

    const data = await response.json();
    res.json(data);
  } catch (error) {
    console.error('Interrupt error:', error);
    res.status(500).json({ error: 'Failed to interrupt' });
  }
});

// End session
app.post('/api/avatar/end-session', async (req, res) => {
  try {
    const { sessionId, token } = req.body;

    if (!sessions.has(sessionId)) {
      return res.status(404).json({ error: 'Session not found' });
    }

    const response = await fetch(`$\{BASE_URL\}/${sessionId}/end`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });

    if (!response.ok) {
      throw new Error(`API error: ${response.status}`);
    }

    sessions.delete(sessionId);

    const data = await response.json();
    res.json(data);
  } catch (error) {
    console.error('End session error:', error);
    res.status(500).json({ error: 'Failed to end session' });
  }
});

// Health check
app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    activeSessions: sessions.size,
  });
});

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
  console.log(`Backend server running on port $\{PORT\}`);
});
```

#### `.env`

```
AVATAR_KS=your-api-key-here
AVATAR_BASE_URL=https://api.avatar.us.kaltura.ai/v1/avatar-session
PORT=3001
```

### Frontend (React)

#### `AvatarDemo.jsx`

```jsx
import { useState, useEffect, useRef } from 'react';
import { KalturaAvatarSession } from '@unisphere/models-sdk-js';

const BACKEND_URL = 'http://localhost:3001';
const AVATAR_BASE_URL = 'https://api.avatar.us.kaltura.ai/';

export default function AvatarDemo() {
  const [session, setSession] = useState(null);
  const [sessionId, setSessionId] = useState(null);
  const [token, setToken] = useState(null);
  const [status, setStatus] = useState('idle');
  const [text, setText] = useState('Hello! How can I help you today?');
  const [error, setError] = useState(null);
  const containerRef = useRef(null);

  const createSession = async () => {
    try {
      setStatus('creating');
      setError(null);

      // Call backend to create session
      const response = await fetch(`$\{BACKEND_URL\}/api/avatar/create-session`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          visualId: 'visual-123',
          voiceId: 'voice-456',
        }),
      });

      if (!response.ok) {
        throw new Error('Failed to create session');
      }

      const data = await response.json();
      setSessionId(data.sessionId);
      setToken(data.token);

      // Use Client SDK for display
      const avatarSession = new KalturaAvatarSession(data.token, {
        baseUrl: AVATAR_BASE_URL,
      });

      avatarSession.on('stateChange', (state) => {
        console.log('State:', state);
        if (state === 'READY') {
          setStatus('ready');
        }
      });

      avatarSession.on('error', (err) => {
        console.error('Avatar error:', err);
        setError(err.message);
        setStatus('error');
      });

      // Create session and attach
      await avatarSession.createSession({
        visualId: 'visual-123',
        videoContainerId: 'avatar-container',
      });

      setSession(avatarSession);

      // Welcome message
      await sayText('Hello! I am your avatar assistant.');
    } catch (err) {
      console.error('Create session error:', err);
      setError(err.message);
      setStatus('error');
    }
  };

  const sayText = async (textToSay) => {
    try {
      // Call backend (not SDK)
      await fetch(`$\{BACKEND_URL\}/api/avatar/say-text`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          sessionId,
          token,
          text: textToSay || text,
        }),
      });
    } catch (err) {
      console.error('Say text error:', err);
      setError(err.message);
    }
  };

  const interrupt = async () => {
    try {
      await fetch(`$\{BACKEND_URL\}/api/avatar/interrupt`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ sessionId, token }),
      });
    } catch (err) {
      console.error('Interrupt error:', err);
      setError(err.message);
    }
  };

  const endSession = async () => {
    try {
      await fetch(`$\{BACKEND_URL\}/api/avatar/end-session`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ sessionId, token }),
      });

      if (session) {
        await session.endSession();
      }

      setSession(null);
      setSessionId(null);
      setToken(null);
      setStatus('idle');
    } catch (err) {
      console.error('End session error:', err);
      setError(err.message);
    }
  };

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      if (session) {
        session.endSession();
      }
    };
  }, [session]);

  return (
    <div style={{ maxWidth: '800px', margin: '50px auto', padding: '20px' }}>
      <h1>Avatar Demo - Server Controlled</h1>

      <div
        style={{
          padding: '12px',
          borderRadius: '6px',
          marginBottom: '20px',
          background: status === 'ready' ? '#d4edda' : '#e9ecef',
        }}
      >
        Status: {status}
        {sessionId && ` (Session: ${sessionId})`}
      </div>

      {error && (
        <div
          style={{
            padding: '12px',
            borderRadius: '6px',
            marginBottom: '20px',
            background: '#f8d7da',
            color: '#721c24',
          }}
        >
          Error: {error}
        </div>
      )}

      <div
        id="avatar-container"
        ref={containerRef}
        style={{
          width: '512px',
          height: '512px',
          border: '2px solid #333',
          borderRadius: '12px',
          overflow: 'hidden',
          background: '#000',
          margin: '20px 0',
        }}
      />

      <div style={{ marginBottom: '20px' }}>
        <button
          onClick={createSession}
          disabled={status !== 'idle'}
          style={{
            padding: '12px 24px',
            marginRight: '10px',
            background: '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: '6px',
            cursor: status === 'idle' ? 'pointer' : 'not-allowed',
          }}
        >
          Start Avatar
        </button>
        <button
          onClick={endSession}
          disabled={status !== 'ready'}
          style={{
            padding: '12px 24px',
            background: '#dc3545',
            color: 'white',
            border: 'none',
            borderRadius: '6px',
            cursor: status === 'ready' ? 'pointer' : 'not-allowed',
          }}
        >
          End Session
        </button>
      </div>

      <textarea
        value={text}
        onChange={(e) => setText(e.target.value)}
        style={{
          width: '100%',
          height: '100px',
          padding: '10px',
          marginBottom: '10px',
          fontSize: '14px',
          borderRadius: '6px',
        }}
      />

      <div>
        <button
          onClick={() => sayText()}
          disabled={status !== 'ready'}
          style={{
            padding: '12px 24px',
            marginRight: '10px',
            background: '#007bff',
            color: 'white',
            border: 'none',
            borderRadius: '6px',
            cursor: status === 'ready' ? 'pointer' : 'not-allowed',
          }}
        >
          Make Avatar Speak
        </button>
        <button
          onClick={interrupt}
          disabled={status !== 'ready'}
          style={{
            padding: '12px 24px',
            background: '#ffc107',
            color: 'black',
            border: 'none',
            borderRadius: '6px',
            cursor: status === 'ready' ? 'pointer' : 'not-allowed',
          }}
        >
          Interrupt
        </button>
      </div>
    </div>
  );
}
```

### Running the Example

#### 1. Backend

```bash
cd backend
npm install express node-fetch cors dotenv
node server.js
```

#### 2. Frontend

```bash
cd frontend
npm install @unisphere/models-sdk-js
npm run dev
```

### Key Benefits

- **Security**: Kaltura Session stays on server
- **Separation**: Backend controls, frontend displays
- **Validation**: Backend validates all requests
- **Audit**: Backend can log all actions
- **Scalable**: Can manage multiple sessions

### Next Steps

- **[React Integration](/docs/conversational-agents/agentic-avatars-sdk/examples-react-integration)** - Reusable React hooks
- **Production Deployment** - Best practices
