OpenTogetherTube uses a role-based permissions system that gives room owners and administrators precise control over who can perform various actions in a room.
Role Hierarchy
Users are assigned roles that determine their permissions:
enum Role {
Administrator = 4 ,
Moderator = 3 ,
TrustedUser = 2 ,
RegisteredUser = 1 ,
UnregisteredUser = 0 ,
Owner = - 1 // Special role, always has all permissions
}
Roles inherit permissions from lower roles. For example, a Moderator has all permissions granted to TrustedUser, RegisteredUser, and UnregisteredUser.
Role Descriptions
The room creator or claimer. Has all permissions and can configure permissions for other roles. Cannot be demoted.
Can promote/demote users up to Administrator, configure room settings, and manage permissions for Moderator and below.
Can promote/demote Trusted Users, kick users, and typically has full queue management permissions.
Registered users promoted for good behavior. Can kick regular users and typically has extended permissions.
Logged-in users. Default permissions depend on room configuration.
Guest users without accounts. Usually have limited permissions.
Permission Types
OpenTogetherTube defines 26 distinct permissions grouped by category:
Playback Control
Permission Description Default Min Role playback.play-pausePlay or pause the video UnregisteredUser playback.skipSkip the current video UnregisteredUser playback.seekSeek to a position UnregisteredUser playback.speedChange playback speed UnregisteredUser
Queue Management
Permission Description Default Min Role manage-queue.addAdd videos to queue UnregisteredUser manage-queue.removeRemove videos from queue UnregisteredUser manage-queue.orderReorder queue items UnregisteredUser manage-queue.voteVote on videos (in vote mode) UnregisteredUser manage-queue.play-nowJump a video to the front UnregisteredUser
Room Configuration
Permission Description Default Min Role configure-room.set-titleChange room title UnregisteredUser configure-room.set-descriptionChange room description UnregisteredUser configure-room.set-visibilityChange public/unlisted/private UnregisteredUser configure-room.set-queue-modeChange queue mode UnregisteredUser configure-room.otherOther settings (SponsorBlock, etc.) UnregisteredUser
Permission Management
Permission Description Default Min Role configure-room.set-permissions.for-all-unregistered-usersConfigure unregistered permissions RegisteredUser configure-room.set-permissions.for-all-registered-usersConfigure registered permissions TrustedUser configure-room.set-permissions.for-trusted-usersConfigure trusted permissions Moderator configure-room.set-permissions.for-moderatorConfigure moderator permissions Administrator
User Management
Permission Description Default Min Role manage-users.promote-trusted-userPromote to Trusted TrustedUser manage-users.demote-trusted-userDemote from Trusted TrustedUser manage-users.promote-moderatorPromote to Moderator Moderator manage-users.demote-moderatorDemote from Moderator Moderator manage-users.promote-adminPromote to Admin Administrator manage-users.demote-adminDemote from Admin Administrator manage-users.kickKick users from room TrustedUser
Communication
Permission Description Default Min Role chatSend chat messages UnregisteredUser
Grants System
Permissions are stored as bitmasks for efficient checking:
export class Permission {
name : PermissionName ;
mask : GrantMask ; // Bit position (1 << n)
minRole : Role ; // Minimum required role
}
// Example permissions
const PERMISSIONS = [
new Permission ({ name: "playback.play-pause" , mask: 1 << 0 }),
new Permission ({ name: "playback.skip" , mask: 1 << 1 }),
new Permission ({ name: "playback.seek" , mask: 1 << 2 }),
// ... 23 more permissions
];
The Grants Class
The Grants class manages permissions for all roles:
export class Grants {
masks : Map < Role , GrantMask > = new Map ();
// Check if a role has a permission
granted ( role : Role , permission : PermissionName ) : boolean {
const checkmask = parseIntoGrantMask ([ permission ]);
const fullmask = this . getMask ( role );
return ( fullmask & checkmask ) === checkmask ;
}
// Throw exception if permission denied
check ( role : Role , permission : PermissionName ) : void {
if ( ! this . granted ( role , permission )) {
throw new PermissionDeniedException ( permission );
}
}
}
Permission Inheritance
Higher roles automatically inherit permissions from lower roles:
private _processInheiritance (): void {
let fullmask : GrantMask = 0 ;
for ( let i = Role . UnregisteredUser ; i <= Role . Administrator ; i ++ ) {
fullmask |= this . getMask ( i ); // OR with previous roles
this . masks . set ( i , fullmask );
}
}
This means you can’t revoke a permission from a higher role that a lower role has. Grant permissions progressively.
Permission Checking
Every room request goes through permission checking:
public async processRequest (
request : RoomRequest ,
context : RoomRequestContext
): Promise < void > {
// Map request types to required permissions
const permissions = new Map ([
[ RoomRequestType . PlaybackRequest , "playback.play-pause" ],
[ RoomRequestType . SkipRequest , "playback.skip" ],
[ RoomRequestType . SeekRequest , "playback.seek" ],
[ RoomRequestType . AddRequest , "manage-queue.add" ],
// ... etc
]);
const permission = permissions . get ( request . type );
if ( permission ) {
this . grants . check ( context . role , permission );
}
// Process request...
}
If permission is denied, a PermissionDeniedException is thrown:
export class PermissionDeniedException extends OttException {
constructor ( permission : string ) {
super ( `Permission denied: ${ permission } ` );
this . name = "PermissionDeniedException" ;
}
}
Configuring Permissions
Room owners can configure permissions through the settings UI or API:
Via API
PATCH /api/room/:name
Content-Type : application/json
{
"grants" : [
[ 0 , 255 ], // UnregisteredUser: basic permissions
[ 1 , 65535 ], // RegisteredUser: more permissions
[ 2 , 16777215 ] // TrustedUser: extensive permissions
]
}
Via UI
The PermissionsEditor component provides a visual interface:
< template >
< PermissionsEditor
v-model = " settings . grants . value "
: current-role = " store . getters [ 'users/self' ]?. role ?? Role . Owner "
/>
</ template >
Using Permission Strings
You can use permission name patterns:
// Grant all playback permissions
parseIntoGrantMask ([ "playback" ])
// Grant specific permissions
parseIntoGrantMask ([ "playback.play-pause" , "playback.seek" ])
// Grant all permissions
parseIntoGrantMask ([ "*" ])
Role Assignment
Getting a User’s Role
getRole ( user ?: RoomUser ): Role {
if ( ! user ) {
return Role . UnregisteredUser ;
}
if ( this . isOwner ( user )) {
return Role . Owner ;
}
if ( user . user ) {
// Check promoted roles (Admin, Mod, Trusted)
for ( let i = Role . Administrator ; i >= Role . TrustedUser ; i -- ) {
if ( this . userRoles . get ( i )?. has ( user . user . id )) {
return i ;
}
}
}
if ( user . isLoggedIn ) {
return Role . RegisteredUser ;
}
return Role . UnregisteredUser ;
}
Room moderators can promote users to higher roles:
public async promoteUser (
request : PromoteRequest ,
context : RoomRequestContext
): Promise < void > {
const targetUser = this . getUser ( request . targetClientId );
const targetCurrentRole = this . getRole ( targetUser );
// Check promotion permission
let perm: string | undefined ;
switch ( request . role ) {
case Role. Administrator:
perm = "manage-users.promote-admin" ;
break ;
case Role . Moderator :
perm = "manage-users.promote-moderator" ;
break ;
case Role . TrustedUser :
perm = "manage-users.promote-trusted-user" ;
break ;
}
if ( perm ) {
this.grants.check(context. role , perm);
}
// Check demotion permission if downgrading
if ( request . role < targetCurrentRole ) {
// Requires demotion permission for target's current role
// ...
}
// Apply role change
if ( targetUser . user_id !== undefined ) {
// Remove from all role sets
for ( let i = Role . Administrator ; i > = Role . TrustedUser ; i --) {
this . userRoles . get ( i )?. delete ( targetUser . user_id );
}
// Add to new role set
if (request.role > = Role . TrustedUser ) {
this . userRoles . get ( request . role )?. add ( targetUser . user_id );
}
}
await this . syncUser ( this . getUserInfo ( targetUser . id ));
}
Only registered users can be promoted. Unregistered users must create an account first.
Client-Side Permission Checks
The client can check permissions before attempting actions:
// In Vue component
import { useGrants } from "@/components/composables/grants" ;
const granted = useGrants ();
// Check if user can add videos
if ( granted ( "manage-queue.add" )) {
// Show add button
}
The useGrants composable:
export function useGrants () {
const store = useStore ();
return ( permission : PermissionName ) : boolean => {
const self = store . getters [ "users/self" ];
if ( ! self ) return false ;
return store . state . room . grants . granted ( self . role , permission );
};
}
Default Permissions
New rooms start with permissive defaults:
function defaultPermissions () : Grants {
return new Grants ({
[Role.UnregisteredUser]: parseIntoGrantMask ([
"playback" ,
"manage-queue" ,
"chat" ,
"configure-room.set-title" ,
"configure-room.set-description" ,
"configure-room.set-visibility" ,
"configure-room.set-queue-mode" ,
"configure-room.other"
]),
[Role.RegisteredUser]: parseIntoGrantMask ([]),
[Role.TrustedUser]: parseIntoGrantMask ([]),
[Role.Moderator]: parseIntoGrantMask ([
"manage-users.promote-trusted-user" ,
"manage-users.demote-trusted-user" ,
"manage-users.kick"
]),
[Role.Administrator]: parseIntoGrantMask ([ "*" ]),
[Role.Owner]: parseIntoGrantMask ([ "*" ])
});
}
This configuration allows anyone to control playback in new rooms, making them immediately usable. Room owners should adjust permissions after claiming.
Kicking Users
Users with the manage-users.kick permission can remove others:
public async kickUser (
request : KickRequest ,
context : RoomRequestContext
): Promise < void > {
const user = this . getUser ( request . clientId );
if (! user ) {
throw new ClientNotFoundInRoomException ( this . name );
}
if ( canKickUser (context.role, this.getRole( user ))) {
this . command ({ type: "kick" , clientId: request . clientId });
}
}
// From userutils.ts
export function canKickUser ( kickerRole : Role , targetRole : Role ) : boolean {
// Can't kick owner
if ( targetRole === Role . Owner ) return false ;
// Can only kick users with lower role
return kickerRole > targetRole ;
}
Storage
Permissions are stored in multiple locations:
Database (Permanent Rooms)
CREATE TABLE " Rooms " (
id SERIAL PRIMARY KEY ,
name VARCHAR ( 32 ) UNIQUE ,
permissions JSONB, -- Serialized Grants
"role-admin" JSONB, -- Array of user IDs
"role-mod" JSONB,
"role-trusted" JSONB,
-- ...
);
Redis (Active Rooms)
interface RoomStateFromRedis {
grants : [ Role , GrantMask ][]; // Serialized as array of tuples
userRoles : [ Role , number []][]; // Role -> user IDs
// ...
}
Serialization
// Grants to JSON
toJSON (): [ Role , GrantMask ][] {
return [ ... this . masks ];
}
// JSON to Grants
deserialize ( value : string ): void {
const g = JSON . parse ( value );
this . setAllGrants ( g );
}
Best Practices
Start Restrictive
Begin with minimal permissions for guests, grant more as needed.
Use Roles Effectively
Promote trusted community members to reduce moderation burden.
Test Permissions
Join your room as a guest to verify permissions work as expected.
Document Your Setup
Communicate your room’s permission structure to users.
Room Management Room creation, ownership, and lifecycle
Video Sync How playback control permissions are enforced
Vote Mode Democratic queue management with voting permissions