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:
interface DailyParticipantPermissions {
  hasPresence: boolean;
  canSend: Set<'video' | 'audio' | 'screenVideo' | 'screenAudio' | 'customVideo' | 'customAudio'> | boolean;
  canReceive: DailyParticipantCanReceivePermission;
  canAdmin: Set<'participants' | 'streaming' | 'transcription'> | boolean;
}
hasPresence
boolean
When false, the participant is hidden from all other participants. They do not appear in participants() for others, cannot be seen, and their events are not broadcast. Useful for silent observers or bots. Hidden participants require SFU topology.
canSend
Set<string> | boolean
Controls which media tracks the participant may publish.Pass true to allow all, false to block all, or a Set of specific values.
canReceive
DailyParticipantCanReceivePermission
Controls which participants’ media streams this participant may subscribe to.Override resolution order (highest to lowest): byParticipantIdbyUserIdbase.
canAdmin
Set<string> | boolean
Controls which administrative capabilities the participant holds.Pass true to grant all admin rights, false to grant none, or a Set of specific values.

Reading permissions

Use usePermissions to reactively read the local participant’s permissions. It returns convenience booleans for each permission type:
import { usePermissions } from '@daily-co/daily-react';

function MediaControls() {
  const {
    canSendAudio,
    canSendVideo,
    canSendScreenVideo,
    canAdminParticipants,
    canAdminTranscription,
  } = usePermissions();

  return (
    <div>
      {canSendAudio && <MicButton />}
      {canSendVideo && <CameraButton />}
      {canSendScreenVideo && <ScreenShareButton />}
      {canAdminTranscription && <TranscriptionButton />}
    </div>
  );
}
To read another participant’s permissions, pass their sessionId to usePermissions:
const { canSendAudio } = usePermissions(participantSessionId);

Updating permissions at runtime

Use useDaily to get the call object and call updateParticipant(). The caller must themselves have canAdmin: 'participants' (or canAdmin: true).
import { useDaily } from '@daily-co/daily-react';

function ParticipantControls({ sessionId }) {
  const daily = useDaily();

  const muteParticipant = () => {
    daily.updateParticipant(sessionId, {
      updatePermissions: {
        canSend: new Set(['video']), // video only, no audio
      },
    });
  };

  const restoreParticipant = () => {
    daily.updateParticipant(sessionId, {
      updatePermissions: {
        canSend: new Set(['audio', 'video']),
      },
    });
  };

  return (
    <>
      <button onClick={muteParticipant}>Mute</button>
      <button onClick={restoreParticipant}>Unmute</button>
    </>
  );
}
The updatePermissions object is partial — only the fields you provide 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

Restrict a participant to audio-only (no camera or screen share):
daily.updateParticipant(sessionId, {
  updatePermissions: {
    canSend: new Set(['audio']),
  },
});

Restricting what a participant can receive

Allow a participant to receive audio from everyone but video only from a specific user:
daily.updateParticipant(sessionId, {
  updatePermissions: {
    canReceive: {
      base: {
        video: false,
        audio: true,
        screenVideo: false,
        screenAudio: false,
        customVideo: { '*': false },
        customAudio: { '*': false },
      },
      byUserId: {
        'presenter-user-id': { video: true },
      },
    },
  },
});

Granting admin permissions

Grant a participant transcription admin rights so they can start and stop transcription:
daily.updateParticipant(sessionId, {
  updatePermissions: {
    canAdmin: new Set(['transcription']),
  },
});

Waiting room (knock-to-join)

When a room has knocking enabled, participants who attempt to join land in a waiting room until a host admits or denies them. Use useWaitingParticipants to manage the waiting room from the host side.

Host: managing waiting participants

useWaitingParticipants returns waitingParticipants, grantAccess, and denyAccess. Pass callbacks to react to participants entering or leaving the lobby:
import { useCallback } from 'react';
import { useWaitingParticipants } from '@daily-co/daily-react';

function WaitingRoom() {
  const onWaitingParticipantAdded = useCallback((event) => {
    console.log(`${event.participant.name} is waiting`);
  }, []);

  const { waitingParticipants, grantAccess, denyAccess } = useWaitingParticipants({
    onWaitingParticipantAdded,
  });

  return (
    <ul>
      {waitingParticipants.map((p) => (
        <li key={p.id}>
          {p.name}
          <button onClick={() => grantAccess(p.id)}>Admit</button>
          <button onClick={() => denyAccess(p.id)}>Deny</button>
        </li>
      ))}
    </ul>
  );
}
To admit or deny all waiting participants at once, pass '*':
grantAccess('*'); // admit everyone
denyAccess('*');  // deny everyone

Guest: checking access state

Use useDaily and useDailyEvent to track the local participant’s access state:
import { useState, useCallback } from 'react';
import { useDaily, useDailyEvent } from '@daily-co/daily-react';

function AccessStateHandler() {
  const daily = useDaily();
  const [screen, setScreen] = useState('joining');

  useDailyEvent('access-state-updated', useCallback(({ access }) => {
    if (typeof access === 'object') {
      if (access.level === 'lobby') setScreen('waiting');
      else if (access.level === 'full') setScreen('call');
      else if (access.level === 'none') setScreen('denied');
    }
  }, []));

  if (screen === 'waiting') return <p>Waiting for the host to admit you…</p>;
  if (screen === 'denied') return <p>Access denied.</p>;
  return null;
}

Example: setting up a waiting room

1

Enable knocking on the room

Create or configure a Daily room with enable_knocking: true via the Daily REST API or dashboard.
2

Listen for waiting participants (host)

import { useCallback } from 'react';
import { useWaitingParticipants } from '@daily-co/daily-react';

function KnockingManager() {
  const onWaitingParticipantAdded = useCallback(({ participant }) => {
    showKnockPrompt(participant);
  }, []);

  const onWaitingParticipantRemoved = useCallback(({ participant }) => {
    dismissKnockPrompt(participant.id);
  }, []);

  const { waitingParticipants, grantAccess, denyAccess } = useWaitingParticipants({
    onWaitingParticipantAdded,
    onWaitingParticipantRemoved,
  });

  return waitingParticipants.map((p) => (
    <KnockPrompt
      key={p.id}
      participant={p}
      onAdmit={() => grantAccess(p.id)}
      onDeny={() => denyAccess(p.id)}
    />
  ));
}
3

Handle access state changes (guest)

import { useState, useCallback } from 'react';
import { useDailyEvent } from '@daily-co/daily-react';

function LobbyScreen() {
  const [accessLevel, setAccessLevel] = useState(null);

  useDailyEvent('access-state-updated', useCallback(({ access }) => {
    if (typeof access === 'object') setAccessLevel(access.level);
  }, []));

  if (accessLevel === 'lobby') return <p>Waiting for the host…</p>;
  if (accessLevel === 'full') return <CallView />;
  if (accessLevel === 'none') return <p>You were not admitted.</p>;
  return null;
}
Participants with hasPresence: false require SFU network topology. Daily automatically switches to SFU mode when hidden participants are present, but your room must support SFU.