Skip to main content
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 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.
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:
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):
call.updateParticipant(participantSessionId, {
  updatePermissions: {
    canSend: new Set(['audio']),
  },
});
To re-enable video later:
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:
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

participant-updated
DailyEventObjectParticipant
Fired when a participant’s permissions change. Check participant.permissions to see the updated values.
call.on('participant-updated', ({ participant }) => {
  console.log('Updated permissions:', participant.permissions);
});

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
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 });
}