5Stack provides comprehensive player moderation tools that allow Match Organizers and Administrators to enforce community standards and manage player behavior. The sanctions system supports multiple penalty types with flexible duration controls.
Overview
Player sanctions are disciplinary actions that restrict player participation or communication. The system tracks active and expired sanctions, abandoned matches, and provides real-time updates via GraphQL subscriptions.
Only users with Match Organizer role or higher can manage player sanctions.
Sanction Types
Ban Complete Restriction Player is not able to participate in any activity on the platform.
Mute Voice Restriction Player cannot use voice chat in game but can still play and use text chat.
Gag Text Restriction Player cannot use text chat in game but can still play and use voice chat.
Silence Full Communication Ban Player is both muted and gagged, unable to use any form of in-game communication.
Creating Sanctions
Sanction Interface
The sanction creation interface provides a streamlined workflow:
< template >
< Popover >
< PopoverTrigger as-child >
< Button variant = "outline" >
{{ $t ( "player.sanction.button" ) }}
< ChevronDownIcon class = "ml-2 h-4 w-4 text-muted-foreground" />
</ Button >
</ PopoverTrigger >
< PopoverContent class = "p-0" align = "end" >
< Command v-model = " sanctionType " >
< CommandList >
< CommandGroup >
< CommandItem
v-for = " ( sanction , type ) in sanctions "
: key = " type "
: value = " type "
@ click = " sanctioningPlayer = true "
>
< div class = "flex items-center gap-2" >
< component : is = " sanction . icon " class = "h-4 w-4" />
< span class = "font-medium capitalize" > {{ type }} </ span >
</ div >
< p class = "text-sm text-muted-foreground mt-1" >
{{ sanction . description }}
</ p >
</ CommandItem >
</ CommandGroup >
</ CommandList >
</ Command >
</ PopoverContent >
</ Popover >
</ template >
Duration Options
Sanctions can be temporary or permanent:
const durations = [
{ label: "15 minutes" , duration: 1000 * 60 * 15 },
{ label: "30 minutes" , duration: 1000 * 60 * 30 },
{ label: "1 hour" , duration: 1000 * 60 * 60 },
{ label: "1 day" , duration: 1000 * 60 * 60 * 24 },
{ label: "1 week" , duration: 1000 * 60 * 60 * 24 * 7 },
{ label: "1 month" , duration: 1000 * 60 * 60 * 24 * 30 },
{ label: "Permanent" , duration: 0 },
];
Creating a Sanction
async function sanctionPlayer () {
if ( ! sanctionType ) return ;
let remove_sanction_date : Date | null = null ;
const currentDate = new Date ();
if ( form . values . duration && form . values . duration !== "0" ) {
remove_sanction_date = new Date (
currentDate . getTime () + parseInt ( form . values . duration )
);
}
await $apollo . mutate ({
mutation: generateMutation ({
insert_player_sanctions_one: [
{
object: {
type: sanctionType ,
player_steam_id: player . steam_id ,
reason: form . values . reason ,
remove_sanction_date ,
},
},
{
id: true ,
},
],
}),
});
toast ({
title: ` ${ sanctionType } ed ${ player . name } ` ,
});
}
Viewing Sanctions
Real-Time Subscription
Sanctions are loaded and updated in real-time:
apollo : {
$subscribe : {
player_sanctions : {
query : typedGql ( "subscription" )({
player_sanctions: [
{
where: {
player_steam_id: {
_eq: $ ( "playerId" , "bigint!" ),
},
},
},
{
id: true ,
type: true ,
reason: true ,
created_at: true ,
remove_sanction_date: true ,
},
],
}),
variables : function () : { playerId : string } {
return {
playerId: this . playerId ,
};
},
result : function ({ data } : { data : any }) {
this . sanctions = data . player_sanctions ;
},
},
},
}
Active vs Expired Sanctions
The system distinguishes between active and expired sanctions:
const activeSanctions = computed (() => {
return sanctions . filter (( sanction ) => {
if ( sanction . remove_sanction_date ) {
return new Date ( sanction . remove_sanction_date ) > new Date ();
}
return true ; // Permanent sanctions are always active
}). length ;
});
Sanctions Display
The sanctions sheet shows a badge indicating the number of active sanctions:
< Button
variant = "ghost"
size = "sm"
: class = " {
'text-destructive hover:text-destructive' : activeSanctions > 0 ,
} "
>
<AlertTriangle class="h-3.5 w-3.5" />
<span>{{ $t("player.sanctions.title") }}</span>
<Badge
v-if="activeSanctions > 0"
variant="destructive"
class="ml-0.5 h-4 px-1.5 text-xs"
>
{{ activeSanctions }}
</Badge>
</ Button >
Managing Sanctions
Editing Sanction Duration
Match Organizers can modify sanction expiration dates:
async function updateSanctionEndTime () {
if ( ! editingSanction ) return ;
let remove_sanction_date : Date | null = null ;
if ( editDate && editTime ) {
const [ hours , minutes ] = editTime . split ( ":" ). map ( Number );
remove_sanction_date = new Date (
Date . UTC (
editDate . year ,
editDate . month - 1 ,
editDate . day ,
hours ,
minutes
)
);
}
try {
await $apollo . mutate ({
mutation: generateMutation ({
update_player_sanctions_by_pk: [
{
pk_columns: {
id: editingSanction . id ,
created_at: editingSanction . created_at ,
},
_set: {
remove_sanction_date ,
},
},
{
id: true ,
remove_sanction_date: true ,
},
],
}),
});
toast ({
title: $t ( "player.sanctions.updated" ),
});
} catch ( error ) {
console . error ( "Failed to update sanction:" , error );
toast ({
title: $t ( "player.sanctions.update_failed" ),
variant: "destructive" ,
});
}
}
Removing Sanctions
Sanctions can be removed entirely:
async function confirmRemoveSanction () {
if ( ! sanctionToDelete ) return ;
try {
await $apollo . mutate ({
mutation: generateMutation ({
delete_player_sanctions_by_pk: [
{
id: sanctionToDelete . id ,
created_at: sanctionToDelete . created_at ,
},
{
id: true ,
},
],
}),
});
toast ({
title: $t ( "player.sanctions.removed" ),
});
sanctionToDelete = null ;
} catch ( error ) {
console . error ( "Failed to remove sanction:" , error );
toast ({
title: $t ( "player.sanctions.remove_failed" ),
variant: "destructive" ,
});
}
}
Abandoned Matches
The system tracks when players abandon matches before completion.
Tracking Abandoned Matches
abandoned_matches : {
query : typedGql ( "subscription" )({
abandoned_matches: [
{
where: {
steam_id: {
_eq: $ ( "playerId" , "bigint!" ),
},
},
order_by: [
{
abandoned_at: order_by . desc ,
},
],
},
{
id: true ,
steam_id: true ,
abandoned_at: true ,
},
],
}),
variables : function () : { playerId : string } {
return {
playerId: this . playerId ,
};
},
result : function ({ data } : { data : any }) {
this . abandonedMatches = data . abandoned_matches ;
},
}
Abandoned Matches Display
Abandoned matches are shown in a separate tab with pagination:
const displayedAbandonedMatches = computed (() => {
if ( ! abandonedMatches || abandonedMatches . length === 0 ) {
return [];
}
const start = ( abandonedMatchesPage - 1 ) * itemsPerPage ;
const end = start + itemsPerPage ;
return abandonedMatches . slice ( start , end );
});
Removing Abandoned Match Records
async function confirmRemoveAbandonedMatch () {
if ( ! abandonedMatchToDelete ) return ;
try {
await $apollo . mutate ({
mutation: generateMutation ({
delete_abandoned_matches_by_pk: [
{
id: abandonedMatchToDelete . id ,
},
{
id: true ,
},
],
}),
});
toast ({
title: $t ( "player.sanctions.abandoned_removed" ),
});
abandonedMatchToDelete = null ;
} catch ( error ) {
console . error ( "Failed to remove abandoned match:" , error );
toast ({
title: $t ( "player.sanctions.abandoned_remove_failed" ),
variant: "destructive" ,
});
}
}
Permission Checks
All sanction management features check for proper permissions:
const canManageSanctions = computed (() => {
return useAuthStore (). isRoleAbove ( e_player_roles_enum . match_organizer );
});
Edit and remove actions are only visible to Match Organizers and Administrators. Regular users and players can view their own sanctions but cannot modify them.
Best Practices
Document Sanction Reasons
Always provide clear, specific reasons when applying sanctions. This helps with accountability and potential appeals.
Start with shorter durations for first-time offenses. Escalate to longer durations or permanent bans for repeat violations.
Periodically review active sanctions to ensure they remain appropriate and haven’t expired due to system issues.
Communication Bans vs Full Bans
Use mute/gag/silence for communication violations. Reserve bans for serious rule violations or repeated offenses.
Consider the number and frequency of abandoned matches. Single incidents may be technical issues rather than behavioral problems.
Roles & Permissions Understand the permission system
Match Overview Learn about match system
Player Profiles View player information and history