> ## Documentation Index
> Fetch the complete documentation index at: https://docs.daily.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Permissions

> Control what participants can send, receive, and administer in a Daily call.

Daily's permissions system lets you control exactly what each participant is allowed to do in a call: which media they can publish, which streams they can receive, and which administrative actions they can perform. Permissions can be set at join time via a meeting token or updated dynamically at runtime.

## DailyParticipantPermissions

Every participant object exposes a `permissions` field of type `DailyParticipantPermissions`:

See the [`DailyParticipantPermissions` reference](/reference/daily-js/types/daily-participant-permissions) for the full interface definition and field descriptions.

## Updating permissions at runtime

Use `updateParticipant()` to change a participant's permissions mid-call. The caller must themselves have `canAdmin: 'participants'` (or `canAdmin: true`) or be an owner.

```typescript theme={null}
call.updateParticipant(sessionId, {
  updatePermissions: {
    hasPresence: true,
    canSend: new Set(['audio', 'video']),
    canAdmin: false,
    canReceive: {
      base: true,
    },
  },
});
```

The `updatePermissions` field accepts a `DailyParticipantPermissionsUpdate`, where every field is optional — only those provided are changed:

```typescript theme={null}
type DailyParticipantPermissionsUpdate = {
  hasPresence?: boolean;
  canSend?: Array<string> | Set<string> | boolean;
  canReceive?: Partial<DailyParticipantCanReceivePermission>;
  canAdmin?: Array<string> | Set<string> | boolean;
};
```

### Restricting what a participant can send

The example below restricts a participant to audio-only (no camera or screen share):

```typescript theme={null}
call.updateParticipant(participantSessionId, {
  updatePermissions: {
    canSend: new Set(['audio']),
  },
});
```

To re-enable video later:

```typescript theme={null}
call.updateParticipant(participantSessionId, {
  updatePermissions: {
    canSend: new Set(['audio', 'video']),
  },
});
```

### Restricting what a participant can receive

The example below allows a participant to receive audio from everyone but video only from a specific user:

```typescript theme={null}
call.updateParticipant(participantSessionId, {
  updatePermissions: {
    canReceive: {
      base: {
        video: false,
        audio: true,
        screenVideo: false,
        screenAudio: false,
        customVideo: { '*': false },
        customAudio: { '*': false },
      },
      byUserId: {
        'presenter-user-id': { video: true },
      },
    },
  },
});
```

## Events

<ResponseField name="participant-updated" type="DailyEventObjectParticipant">
  Fired when a participant's permissions change. Check `participant.permissions` to see the updated values.

  ```typescript theme={null}
  call.on('participant-updated', ({ participant }) => {
    console.log('Updated permissions:', participant.permissions);
  });
  ```
</ResponseField>

## Complete example

Demonstrates the full permissions lifecycle: new participants join locked out, request presence via an app message, get admitted by the owner, and then have their `canSend` permissions managed through a panel.

* Open `https://localhost?t=YOUR_OWNER_TOKEN` for the owner view
* Open `https://localhost` (no token) for a participant view

<CodeGroup>
  ```javascript app.js theme={null}
  const ROOM_URL = 'https://your-domain.daily.co/room';

  const token = new URLSearchParams(window.location.search).get('t') ?? undefined;
  let canAdmin = false;
  const call = Daily.createCallObject();
  window.call = call; // for debugging
  const participantIds = new Set();
  window.participantIds = participantIds; // for debugging

  const SEND_TRACKS = ['audio', 'video'];

  // ── Helpers ──────────────────────────────────────────────────────────────────

  function _canAdmin(permissions) {
    const ca = permissions?.canAdmin;
    if (ca === true) return true;
    if (!ca) return false;
    return ca instanceof Set ? ca.has('participants') : false;
  }

  function canSendSet(permissions) {
    const cs = permissions?.canSend;
    if (cs === true) return new Set(SEND_TRACKS);
    if (!cs) return new Set();
    return cs instanceof Set ? new Set(cs) : new Set(cs);
  }

  // ── Track rendering (shared) ─────────────────────────────────────────────────

  function addParticipantTile(participant) {
    const sessionId = participant.session_id;
    let tile = document.getElementById(`tile-${sessionId}`);
    if (tile) return tile;
    tile = document.createElement('div');
    tile.id = `tile-${sessionId}`;
    tile.className = 'tile';

    const label = document.createElement('span');
    label.className = 'tile-label';
    console.log(call.participants());
    label.textContent = participant.local ? 'You' : sessionId.slice(0, 8);

    tile.append(label);
    document.getElementById('tracks').appendChild(tile);
    return tile;
  }

  function addVideoTile(participant, track) {
    const tile = addParticipantTile(participant);
    if (!tile) return;

    let video = tile.querySelector('video');
    if (video) {
      video.srcObject = new MediaStream([track]);
      return;
    }
    video = document.createElement('video');
    video.autoplay = true;
    video.playsInline = true;
    video.srcObject = new MediaStream([track]);

    tile.append(video);
  }

  function addAudioTile(participant, track) {
    const tile = addParticipantTile(participant);
    if (!tile) return;

    let audio = tile.querySelector('audio');
    if (audio) {
      audio.srcObject = new MediaStream([track]);
      return;
    }
    audio = document.createElement('audio');
    audio.autoplay = true;
    audio.srcObject = new MediaStream([track]);
    tile.append(audio);
  }

  call.on('track-started', ({ participant, track, type }) => {
    console.log('track started', { participant, track, type });
    if (type === 'video') addVideoTile(participant, track);
    if (type === 'audio' && !participant.local) addAudioTile(participant, track);
  });

  call.on('track-stopped', ({ participant, type }) => {
    if (type === 'video')
      document.getElementById(`tile-${participant.session_id}`)?.remove();
  });

  // ── Owner panel ──────────────────────────────────────────────────────────────

  let pendingSessionId = null;

  function populateDropdown() {
    const select = document.getElementById('participant-select');
    let prev = select.value;
    console.log(`Select.value: ${select.value}`);
    select.innerHTML = '<option value="">— select participant —</option>';

    for (const [id, p] of Object.entries(call.participants())) {
      if (p.local) continue;
      const opt = document.createElement('option');
      opt.value = id;
      opt.textContent = p.user_name || id.slice(0, 8);
      select.appendChild(opt);
    }

    select.value = prev;
    syncCheckboxes();
  }

  function syncCheckboxes() {
    const id = document.getElementById('participant-select').value;
    const participant = id ? call.participants()[id] : null;
    const perms = participant?.permissions;
    const sending = canSendSet(perms);

    document.getElementById('check-hasPresence').checked =
      perms?.hasPresence ?? false;
    for (const track of SEND_TRACKS) {
      document.getElementById(`check-${track}`).checked = sending.has(track);
    }
  }

  document
    .getElementById('participant-select')
    .addEventListener('change', syncCheckboxes);

  document.getElementById('update-btn').addEventListener('click', async () => {
    const id = document.getElementById('participant-select').value;
    if (!id) return;

    const hasPresence = document.getElementById('check-hasPresence').checked;
    const canSend = new Set(
      SEND_TRACKS.filter((t) => document.getElementById(`check-${t}`).checked),
    );

    console.log('Updating permissions for', id, { hasPresence, canSend });
    await call.updateParticipant(id, {
      updatePermissions: { hasPresence, canSend },
    });
  });

  call.on('app-message', ({ data }) => {
    if (data?.type !== 'request-presence') return;
    pendingSessionId = data.sessionId;
    document.getElementById('request-text').textContent =
      `Participant ${data.sessionId.slice(0, 8)} is requesting presence.`;
    document.getElementById('request-popup').hidden = false;
  });

  document.getElementById('approve-btn').addEventListener('click', async () => {
    if (!pendingSessionId) return;
    // Grant presence only — canSend stays false until the owner enables it via the panel
    console.log('Granting presence to', pendingSessionId);
    await call.updateParticipant(pendingSessionId, {
      updatePermissions: { hasPresence: true },
    });
  });

  document.getElementById('deny-btn').addEventListener('click', () => {
    pendingSessionId = null;
    document.getElementById('request-popup').hidden = true;
  });

  // ── Participant panel ─────────────────────────────────────────────────────────

  function updatePermsDisplay(permissions) {
    const sending = canSendSet(permissions);
    console.log('canSend:', sending);
    const rows = [
      ['Has presence', permissions?.hasPresence],
      ['Audio', sending.has('audio')],
      ['Video', sending.has('video')],
    ];
    document.getElementById('perms-list').innerHTML = rows
      .map(
        ([label, on]) =>
          `<li class="${on ? 'on' : 'off'}">${label}: ${on ? '✓' : '✗'}</li>`,
      )
      .join('');
  }

  document.getElementById('request-btn').addEventListener('click', () => {
    call.sendAppMessage(
      {
        type: 'request-presence',
        sessionId: call.participants().local.session_id,
      },
      '*',
    );
    document.getElementById('request-btn').textContent = 'Request sent…';
    document.getElementById('request-btn').disabled = true;
  });

  // ── Shared event handlers ─────────────────────────────────────────────────────

  call.on('participant-joined', (e) => {
    const participant = e.participant;
    console.log('Participant joined', e);
    if (canAdmin) {
      console.log(
        'Participant joined:',
        participantIds,
        _canAdmin(participant.permissions),
      );
      if (!participantIds.has(participant.session_id)) {
        if (!_canAdmin(participant.permissions)) {
          // Lock out every new non-admin participant immediately
          console.log('Denying presence to', participant.session_id);
          call.updateParticipant(participant.session_id, {
            updatePermissions: { hasPresence: false, canSend: false },
          });
        }
        participantIds.add(participant.session_id);
      } else if (pendingSessionId === participant.session_id) {
        pendingSessionId = null;
        document.getElementById('request-popup').hidden = true;
      }
      populateDropdown();
    }
  });

  call.on('joined-meeting', () => {
    canAdmin = _canAdmin(call.participants().local.permissions);

    if (canAdmin) {
      document.getElementById('owner-panel').hidden = false;
      populateDropdown();
    } else {
      document.getElementById('participant-panel').hidden = false;
      updatePermsDisplay(call.participants().local?.permissions);
    }
  });

  call.on('participant-updated', ({ participant }) => {
    if (canAdmin && !participant.local) {
      populateDropdown();
    }
    if (!canAdmin && participant.local) {
      updatePermsDisplay(participant.permissions);
      // Re-enable request button if presence was revoked
      if (!participant.permissions?.hasPresence) {
        document.getElementById('request-btn').textContent = 'Request presence';
        document.getElementById('request-btn').disabled = false;
      }
      const canSend = canSendSet(participant.permissions);
      if (canSend.has('video') && !call.localVideo()) call.setLocalVideo(true);
      if (canSend.has('audio') && !call.localAudio()) call.setLocalAudio(true);
    }
  });

  call.on('participant-left', ({ participant, reason }) => {
    document.getElementById(`tile-${participant.session_id}`)?.remove();
    if (canAdmin) {
      if (reason !== 'hidden') {
        participantIds.delete(participant.session_id);
      }
      populateDropdown();
    }
  });

  // ── Join ──────────────────────────────────────────────────────────────────────

  if (token) {
    await call.join({ url: ROOM_URL, token });
  } else {
    await call.join({ url: ROOM_URL });
  }
  ```

  ```html index.html theme={null}
  <!doctype html>
  <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <title>Permissions Demo</title>
      <link rel="stylesheet" href="styles.css" />
    </head>
    <body>
      <div id="app">
        <!-- Main area: video tiles for all visible participants -->
        <div id="tracks"></div>

        <!-- Owner panel -->
        <aside id="owner-panel" hidden>
          <h2>Participants</h2>
          <select id="participant-select">
            <option value="">— select participant —</option>
          </select>
          <div id="permission-checkboxes">
            <label
              ><input type="checkbox" id="check-hasPresence" /> Has
              presence</label
            >
            <label><input type="checkbox" id="check-audio" /> Send audio</label>
            <label><input type="checkbox" id="check-video" /> Send video</label>
          </div>
          <button id="update-btn">Update permissions</button>

          <!-- Popup shown when a participant requests presence -->
          <div id="request-popup" hidden>
            <p id="request-text"></p>
            <button id="approve-btn">Approve</button>
            <button id="deny-btn">Deny</button>
          </div>
        </aside>

        <!-- Participant panel -->
        <aside id="participant-panel" hidden>
          <h2>Your permissions</h2>
          <ul id="perms-list"></ul>
          <button id="request-btn">Request presence</button>
        </aside>
      </div>

      <script src="https://unpkg.com/@daily-co/daily-js"></script>
      <script type="module" src="app.js"></script>
    </body>
  </html>
  ```

  ```css styles.css theme={null}
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  [hidden] {
    display: none !important;
  }

  body {
    font-family: sans-serif;
    background: #0f0f1a;
    color: #e0e0e0;
    margin: 0;
    height: 100vh;
    overflow: hidden;
  }

  #app {
    display: flex;
    height: 100%;
  }

  /* ── Tracks area ── */
  #tracks {
    flex: 1;
    display: flex;
    flex-wrap: wrap;
    align-content: flex-start;
    gap: 0.5rem;
    padding: 1rem;
    overflow-y: auto;
  }

  .tile {
    position: relative;
    width: 280px;
    height: 210px;
    background: #1a1a2e;
    border-radius: 6px;
    overflow: hidden;
  }

  .tile video {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .tile-label {
    position: absolute;
    bottom: 6px;
    left: 8px;
    font-size: 0.75rem;
    background: rgba(0, 0, 0, 0.55);
    padding: 2px 6px;
    border-radius: 3px;
  }

  /* ── Side panel ── */
  aside {
    width: 260px;
    flex-shrink: 0;
    background: #13131f;
    border-left: 1px solid #2a2a3e;
    padding: 1rem;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
  }

  h2 {
    margin: 0;
    font-size: 0.9rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: #a0a0c0;
  }

  select {
    width: 100%;
    background: #1a1a2e;
    color: #e0e0e0;
    border: 1px solid #2a2a3e;
    border-radius: 4px;
    padding: 0.4rem;
    font-size: 0.875rem;
  }

  #permission-checkboxes {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    font-size: 0.875rem;
  }

  #permission-checkboxes label {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    cursor: pointer;
  }

  button {
    padding: 0.45rem 0.9rem;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 0.875rem;
    background: #e94560;
    color: #fff;
  }

  button:disabled {
    opacity: 0.45;
    cursor: not-allowed;
  }

  #update-btn {
    background: #2ecc71;
    color: #fff;
  }

  /* ── Request popup ── */
  #request-popup {
    background: #1a1a2e;
    border: 1px solid #2a2a3e;
    border-radius: 6px;
    padding: 0.75rem;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
  }

  #request-text {
    margin: 0;
    font-size: 0.875rem;
  }

  #request-popup div {
    display: flex;
    gap: 0.5rem;
  }

  #approve-btn {
    background: #2ecc71;
  }
  #deny-btn {
    background: #e94560;
  }

  /* ── Permissions list ── */
  #perms-list {
    list-style: none;
    margin: 0;
    padding: 0;
    font-size: 0.875rem;
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
  }

  #perms-list li.on {
    color: #2ecc71;
  }
  #perms-list li.off {
    color: #555577;
  }
  ```
</CodeGroup>
