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 ;
}
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.
Controls which media tracks the participant may publish. Any custom video track started with startCustomTrack().
Any custom audio track started with startCustomTrack().
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. base
DailyParticipantCanReceiveMediaPermissionFull | boolean
required
Default receive permission that applies to all participants unless overridden. true allows all media from all participants, false blocks all. Or provide a granular object: {
video : boolean ;
audio : boolean ;
screenVideo : boolean ;
screenAudio : boolean ;
customVideo : { '*' : boolean ; [ trackName : string ]: boolean };
customAudio : { '*' : boolean ; [ trackName : string ]: boolean };
}
byUserId
Record<string, DailyParticipantCanReceiveMediaPermissionPartial | boolean>
Per-user overrides keyed by user_id. Values are partial permission objects — only the fields you specify override the base.
byParticipantId
Record<string, DailyParticipantCanReceiveMediaPermissionPartial | boolean>
Per-session overrides keyed by session_id. Takes priority over byUserId and base.
Override resolution order (highest to lowest): byParticipantId → byUserId → base.
Controls which administrative capabilities the participant holds. Can mute, eject, and update permissions of other participants. Can also admit waiting participants.
Can start and stop cloud recordings and live streams.
Can start and stop transcription.
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
Enable knocking on the room
Create or configure a Daily room with enable_knocking: true via the Daily REST API or dashboard.
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 ) }
/>
));
}
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.