In short
Put the ranks and every action in a server Script. Clients talk to it through one RemoteFunction, and its OnServerInvoke checks the caller's rank by user ID before it does anything. Save bans in a data store with an end time and check them in Players.PlayerAdded. Publish each new ban on a MessagingService topic so every running server kicks that player right away. The panel itself is a LocalScript that only builds for staff.
- Never trust the panel: a cheater can fire your RemoteFunction with anything, so the server checks the rank, the action and every argument.
- Staff can only act on players below their own rank, so a moderator cannot ban the owner.
- Roblox also has a built-in ban API,
Players:BanAsync, that blocks alt accounts too. When to use it.
Get the script
Admin panel server script with ranks and bans
One server Script holds the ranks, kicks, saves timed and permanent bans in a data store and uses MessagingService to remove a banned player from every server.
-- Script in ServerScriptService named AdminServer.
-- Ranks, server-side checks, kick, timed bans saved in a DataStore,
-- and MessagingService so a ban removes the player from every server at once.
local DataStoreService = game:GetService("DataStoreService")
local MessagingService = game:GetService("MessagingService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
-- Ranks by user ID. 1 = Moderator, 2 = Admin, 3 = Owner. Everyone else is 0.
local RANKS: { [number]: number } = {
[123456] = 3,
}
local RANK_NAMES = { [0] = "Player", [1] = "Moderator", [2] = "Admin", [3] = "Owner" }
-- The lowest rank that may use each action
local MIN_RANK: { [string]: number } = {
kick = 1,
ban = 2,
unban = 2,
}
-- Fixed reasons: the banned player sees one of these. Typed text shown to another
-- player would have to go through Roblox's text filter first, so the panel never sends any.
local REASONS = { "Exploiting", "Harassing players", "Spamming", "Breaking the game rules" }
local MAX_BAN_MINUTES = 60 * 24 * 365
local BAN_TOPIC = "AdminBans"
local bans = DataStoreService:GetDataStore("AdminBans_v1")
local remote = Instance.new("RemoteFunction")
remote.Name = "AdminRemote"
remote.Parent = ReplicatedStorage
local function rankOf(userId: number): number
-- In a Studio playtest of a place you own yourself, you are the Owner
if RunService:IsStudio() and game.CreatorType == Enum.CreatorType.User and userId == game.CreatorId then
return 3
end
return RANKS[userId] or 0
end
local function findPlayer(text: string): Player?
local lower = string.lower(text)
if lower == "" then
return nil
end
for _, player in Players:GetPlayers() do
if string.sub(string.lower(player.Name), 1, #lower) == lower
or string.sub(string.lower(player.DisplayName), 1, #lower) == lower
then
return player
end
end
return nil
end
local function banMessage(reason: string, untilTime: number): string
if untilTime < 0 then
return "You are banned: " .. reason
end
local minutes = math.max(1, math.ceil((untilTime - os.time()) / 60))
return "You are banned for " .. minutes .. " more minutes: " .. reason
end
-- Returns the active ban for a user ID, if any
local function getBan(userId: number): (string?, number?)
local ok, data = pcall(function()
return bans:GetAsync(tostring(userId))
end)
if not ok or type(data) ~= "table" then
return nil, nil
end
local untilTime = tonumber(data.untilTime) or 0
if untilTime >= 0 and untilTime <= os.time() then
return nil, nil -- it ran out
end
return tostring(data.reason or "No reason given"), untilTime
end
-- Every server listens for bans made on other servers
local subscribed, subscribeErr = pcall(function()
MessagingService:SubscribeAsync(BAN_TOPIC, function(message)
local data = message.Data
if type(data) ~= "table" then
return
end
local target = Players:GetPlayerByUserId(tonumber(data.userId) or 0)
if target then
target:Kick(banMessage(tostring(data.reason), tonumber(data.untilTime) or -1))
end
end)
end)
if not subscribed then
warn("AdminServer: cross-server bans are off:", subscribeErr)
end
type Request = { action: string?, target: string?, reason: string?, minutes: number? }
local actions: { [string]: (caller: Player, request: Request) -> string } = {
kick = function(caller, request)
local target = findPlayer(request.target or "")
if not target then
return "No player in this server matches that name."
end
if rankOf(target.UserId) >= rankOf(caller.UserId) then
return "You can only kick players below your rank."
end
target:Kick("Kicked by a moderator: " .. (request.reason or REASONS[#REASONS]))
return "Kicked " .. target.Name .. "."
end,
ban = function(caller, request)
-- the target can be a name in this server or a user ID for someone offline
local target = findPlayer(request.target or "")
local userId = if target then target.UserId else tonumber(request.target)
if not userId then
return "Type the name of a player in this server, or a user ID."
end
if rankOf(userId) >= rankOf(caller.UserId) then
return "You can only ban players below your rank."
end
local minutes = math.floor(tonumber(request.minutes) or 0)
local untilTime = if minutes > 0 then os.time() + math.min(minutes, MAX_BAN_MINUTES) * 60 else -1
local reason = request.reason or REASONS[#REASONS]
local ok, err = pcall(function()
bans:SetAsync(tostring(userId), { untilTime = untilTime, reason = reason, by = caller.UserId })
end)
if not ok then
return "The ban was not saved: " .. tostring(err)
end
if target then
target:Kick(banMessage(reason, untilTime))
end
pcall(function()
MessagingService:PublishAsync(BAN_TOPIC, { userId = userId, reason = reason, untilTime = untilTime })
end)
return "Banned " .. userId .. (if untilTime < 0 then " for good." else " for " .. minutes .. " minutes.")
end,
unban = function(_caller, request)
local userId = tonumber(request.target)
if not userId then
return "Unban takes a user ID."
end
local ok, err = pcall(function()
bans:RemoveAsync(tostring(userId))
end)
return if ok then "Unbanned " .. userId .. "." else "Unban failed: " .. tostring(err)
end,
}
-- The only way in from clients. Every check happens here, never in the panel.
remote.OnServerInvoke = function(caller: Player, request: any): any
local rank = rankOf(caller.UserId)
if type(request) ~= "table" then
-- the panel asks once on join: which rank am I, and which reasons can I pick
return { rank = rank, rankName = RANK_NAMES[rank], reasons = if rank > 0 then REASONS else {} }
end
local action = request.action
local handler = if type(action) == "string" then actions[action] else nil
if not handler or type(action) ~= "string" then
return { ok = false, message = "Unknown action." }
end
if rank < (MIN_RANK[action] or math.huge) then
return { ok = false, message = "Your rank cannot do that." }
end
local clean: Request = {
target = if type(request.target) == "string" then string.sub(request.target, 1, 40) else nil,
reason = if table.find(REASONS, request.reason) then request.reason else nil,
minutes = if type(request.minutes) == "number" then request.minutes else nil,
}
local message = handler(caller, clean)
print(("[Admin] %s (%d) %s %s: %s"):format(caller.Name, caller.UserId, action, clean.target or "", message))
return { ok = true, message = message }
end
-- Banned players are removed as soon as they join
Players.PlayerAdded:Connect(function(player: Player)
local reason, untilTime = getBan(player.UserId)
if reason and untilTime then
player:Kick(banMessage(reason, untilTime))
end
end)
RoCode builds it in the place you have open in Roblox Studio, on Windows or macOS with a desktop browser; on a phone the button sends the prompt and the link to your computer. Free to start, with free credits every day. More Roblox Studio scripts
Where to put it
ServerScriptService
AdminServer Script new
StarterPlayer
StarterPlayerScripts
AdminPanel LocalScript new
Settings you can change
RANKSIn the script- User IDs and their rank: 1 Moderator, 2 Admin, 3 Owner. Replace 123456 with your own ID. Default:
[123456] = 3. MIN_RANKIn the script- The lowest rank allowed to kick, ban and unban. By default moderators can kick and admins can ban.
Add the AdminPanel LocalScript from Step 2 for the on-screen panel. To test bans in Studio, turn on Enable Studio Access to API Services (File, then Experience Settings, then Security).
What a cheater can and cannot doThe server checks the caller's rank and the target's rank on every request, and only accepts fixed reasons, so a cheater who fires the remote gets "Your rank cannot do that."
Type-checked with luau-lsp strict against Roblox's API definitions on 25 October 2026. Not playtested in Studio yet: press Play and watch the Output window before you publish.
Prompt for RoCode
Add an admin panel to my game: ranks by user ID (moderator, admin, owner), a panel only staff can see with a player name box, a reason picker and Kick, Ban and Unban buttons, every check on the server, timed and permanent bans saved in a DataStore, and MessagingService so a ban kicks the player from every server.
On this page
How it fits together
Two scripts and one remote. The panel asks, the server decides.
| Piece | Runs on | Job |
|---|---|---|
| AdminServer | Server | Ranks, the rank checks, kick, ban, unban, the ban check on join and the cross-server listener |
| AdminRemote | ReplicatedStorage | A RemoteFunction the server creates. The panel calls it and gets a message back |
| AdminPanel | Each staff member's screen | Asks the server for its rank on join, and builds the panel only when the rank is 1 or more |
| Data store AdminBans_v1 | Roblox | One entry per banned user ID: when the ban ends, the reason and who banned them |
| Topic AdminBans | Roblox | A MessagingService channel every server listens on, so a ban reaches players in other servers |
| Rank | Number | Can |
|---|---|---|
| Moderator | 1 | Kick |
| Admin | 2 | Kick, ban, unban |
| Owner | 3 | Everything, and act on admins |
Step 1: The server script
- Insert a Script in ServerScriptService named
AdminServerand paste the code below. - Put your user ID in
RANKSwith rank 3, and add your staff with 1 or 2. Your user ID is the number in your profile link, roblox.com/users/123456/profile.
-- Script in ServerScriptService named AdminServer.
-- Ranks, server-side checks, kick, timed bans saved in a DataStore,
-- and MessagingService so a ban removes the player from every server at once.
local DataStoreService = game:GetService("DataStoreService")
local MessagingService = game:GetService("MessagingService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
-- Ranks by user ID. 1 = Moderator, 2 = Admin, 3 = Owner. Everyone else is 0.
local RANKS: { [number]: number } = {
[123456] = 3,
}
local RANK_NAMES = { [0] = "Player", [1] = "Moderator", [2] = "Admin", [3] = "Owner" }
-- The lowest rank that may use each action
local MIN_RANK: { [string]: number } = {
kick = 1,
ban = 2,
unban = 2,
}
-- Fixed reasons: the banned player sees one of these. Typed text shown to another
-- player would have to go through Roblox's text filter first, so the panel never sends any.
local REASONS = { "Exploiting", "Harassing players", "Spamming", "Breaking the game rules" }
local MAX_BAN_MINUTES = 60 * 24 * 365
local BAN_TOPIC = "AdminBans"
local bans = DataStoreService:GetDataStore("AdminBans_v1")
local remote = Instance.new("RemoteFunction")
remote.Name = "AdminRemote"
remote.Parent = ReplicatedStorage
local function rankOf(userId: number): number
-- In a Studio playtest of a place you own yourself, you are the Owner
if RunService:IsStudio() and game.CreatorType == Enum.CreatorType.User and userId == game.CreatorId then
return 3
end
return RANKS[userId] or 0
end
local function findPlayer(text: string): Player?
local lower = string.lower(text)
if lower == "" then
return nil
end
for _, player in Players:GetPlayers() do
if string.sub(string.lower(player.Name), 1, #lower) == lower
or string.sub(string.lower(player.DisplayName), 1, #lower) == lower
then
return player
end
end
return nil
end
local function banMessage(reason: string, untilTime: number): string
if untilTime < 0 then
return "You are banned: " .. reason
end
local minutes = math.max(1, math.ceil((untilTime - os.time()) / 60))
return "You are banned for " .. minutes .. " more minutes: " .. reason
end
-- Returns the active ban for a user ID, if any
local function getBan(userId: number): (string?, number?)
local ok, data = pcall(function()
return bans:GetAsync(tostring(userId))
end)
if not ok or type(data) ~= "table" then
return nil, nil
end
local untilTime = tonumber(data.untilTime) or 0
if untilTime >= 0 and untilTime <= os.time() then
return nil, nil -- it ran out
end
return tostring(data.reason or "No reason given"), untilTime
end
-- Every server listens for bans made on other servers
local subscribed, subscribeErr = pcall(function()
MessagingService:SubscribeAsync(BAN_TOPIC, function(message)
local data = message.Data
if type(data) ~= "table" then
return
end
local target = Players:GetPlayerByUserId(tonumber(data.userId) or 0)
if target then
target:Kick(banMessage(tostring(data.reason), tonumber(data.untilTime) or -1))
end
end)
end)
if not subscribed then
warn("AdminServer: cross-server bans are off:", subscribeErr)
end
type Request = { action: string?, target: string?, reason: string?, minutes: number? }
local actions: { [string]: (caller: Player, request: Request) -> string } = {
kick = function(caller, request)
local target = findPlayer(request.target or "")
if not target then
return "No player in this server matches that name."
end
if rankOf(target.UserId) >= rankOf(caller.UserId) then
return "You can only kick players below your rank."
end
target:Kick("Kicked by a moderator: " .. (request.reason or REASONS[#REASONS]))
return "Kicked " .. target.Name .. "."
end,
ban = function(caller, request)
-- the target can be a name in this server or a user ID for someone offline
local target = findPlayer(request.target or "")
local userId = if target then target.UserId else tonumber(request.target)
if not userId then
return "Type the name of a player in this server, or a user ID."
end
if rankOf(userId) >= rankOf(caller.UserId) then
return "You can only ban players below your rank."
end
local minutes = math.floor(tonumber(request.minutes) or 0)
local untilTime = if minutes > 0 then os.time() + math.min(minutes, MAX_BAN_MINUTES) * 60 else -1
local reason = request.reason or REASONS[#REASONS]
local ok, err = pcall(function()
bans:SetAsync(tostring(userId), { untilTime = untilTime, reason = reason, by = caller.UserId })
end)
if not ok then
return "The ban was not saved: " .. tostring(err)
end
if target then
target:Kick(banMessage(reason, untilTime))
end
pcall(function()
MessagingService:PublishAsync(BAN_TOPIC, { userId = userId, reason = reason, untilTime = untilTime })
end)
return "Banned " .. userId .. (if untilTime < 0 then " for good." else " for " .. minutes .. " minutes.")
end,
unban = function(_caller, request)
local userId = tonumber(request.target)
if not userId then
return "Unban takes a user ID."
end
local ok, err = pcall(function()
bans:RemoveAsync(tostring(userId))
end)
return if ok then "Unbanned " .. userId .. "." else "Unban failed: " .. tostring(err)
end,
}
-- The only way in from clients. Every check happens here, never in the panel.
remote.OnServerInvoke = function(caller: Player, request: any): any
local rank = rankOf(caller.UserId)
if type(request) ~= "table" then
-- the panel asks once on join: which rank am I, and which reasons can I pick
return { rank = rank, rankName = RANK_NAMES[rank], reasons = if rank > 0 then REASONS else {} }
end
local action = request.action
local handler = if type(action) == "string" then actions[action] else nil
if not handler or type(action) ~= "string" then
return { ok = false, message = "Unknown action." }
end
if rank < (MIN_RANK[action] or math.huge) then
return { ok = false, message = "Your rank cannot do that." }
end
local clean: Request = {
target = if type(request.target) == "string" then string.sub(request.target, 1, 40) else nil,
reason = if table.find(REASONS, request.reason) then request.reason else nil,
minutes = if type(request.minutes) == "number" then request.minutes else nil,
}
local message = handler(caller, clean)
print(("[Admin] %s (%d) %s %s: %s"):format(caller.Name, caller.UserId, action, clean.target or "", message))
return { ok = true, message = message }
end
-- Banned players are removed as soon as they join
Players.PlayerAdded:Connect(function(player: Player)
local reason, untilTime = getBan(player.UserId)
if reason and untilTime then
player:Kick(banMessage(reason, untilTime))
end
end)
OnServerInvoke is the only door in. It looks up the caller's rank, refuses actions the rank does not allow, and copies only the fields it expects from the request: a target of at most 40 characters, a reason from the fixed list and a number of minutes. The handlers then check the target's rank against the caller's.
A ban stores untilTime, a Unix time from os.time(), or -1 for a permanent ban. getBan treats a ban whose time has passed as gone, so timed bans end by themselves.
Step 2: The panel
- Insert a LocalScript in StarterPlayer, then StarterPlayerScripts, named
AdminPanel, and paste the code below. - Press Play in a place you own. In Studio you count as the Owner, so an Admin button appears at the top right.
-- LocalScript in StarterPlayer > StarterPlayerScripts named AdminPanel.
-- Builds the panel only for players with a rank. It sends requests; the server decides.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local player = Players.LocalPlayer
local remote = ReplicatedStorage:WaitForChild("AdminRemote") :: RemoteFunction
local ok, info = pcall(function()
return remote:InvokeServer(nil)
end)
if not ok or type(info) ~= "table" or (tonumber(info.rank) or 0) < 1 then
return -- not staff: no panel at all
end
local reasons: { string } = if type(info.reasons) == "table" then info.reasons else { "Breaking the game rules" }
local gui = Instance.new("ScreenGui")
gui.Name = "AdminPanel"
gui.ResetOnSpawn = false
gui.Parent = player:WaitForChild("PlayerGui")
local function corner(parent: Instance, radius: number)
local c = Instance.new("UICorner")
c.CornerRadius = UDim.new(0, radius)
c.Parent = parent
end
local function label(parent: Instance, text: string, y: number): TextLabel
local l = Instance.new("TextLabel")
l.BackgroundTransparency = 1
l.Position = UDim2.fromOffset(16, y)
l.Size = UDim2.new(1, -32, 0, 18)
l.Font = Enum.Font.GothamMedium
l.TextSize = 14
l.TextColor3 = Color3.fromRGB(170, 175, 190)
l.TextXAlignment = Enum.TextXAlignment.Left
l.Text = text
l.Parent = parent
return l
end
local function box(parent: Instance, placeholder: string, y: number): TextBox
local b = Instance.new("TextBox")
b.Position = UDim2.fromOffset(16, y)
b.Size = UDim2.new(1, -32, 0, 36)
b.BackgroundColor3 = Color3.fromRGB(32, 34, 44)
b.TextColor3 = Color3.new(1, 1, 1)
b.PlaceholderText = placeholder
b.PlaceholderColor3 = Color3.fromRGB(120, 125, 140)
b.Font = Enum.Font.Gotham
b.TextSize = 16
b.Text = ""
b.ClearTextOnFocus = false
b.Parent = parent
corner(b, 8)
return b
end
local function button(parent: Instance, text: string, color: Color3, x: number, y: number, width: number): TextButton
local b = Instance.new("TextButton")
b.Position = UDim2.new(x, if x > 0 then 4 else 16, 0, y)
b.Size = UDim2.new(width, -20, 0, 38)
b.BackgroundColor3 = color
b.TextColor3 = Color3.new(1, 1, 1)
b.Font = Enum.Font.GothamBold
b.TextSize = 16
b.Text = text
b.AutoButtonColor = true
b.Parent = parent
corner(b, 8)
return b
end
-- The toggle button, top right of the screen
local toggle = button(gui, "Admin", Color3.fromRGB(90, 70, 200), 0, 0, 0)
toggle.AnchorPoint = Vector2.new(1, 0)
toggle.Position = UDim2.new(1, -16, 0, 60)
toggle.Size = UDim2.fromOffset(90, 36)
local panel = Instance.new("Frame")
panel.Name = "Panel"
panel.AnchorPoint = Vector2.new(1, 0)
panel.Position = UDim2.new(1, -16, 0, 104)
panel.Size = UDim2.fromOffset(320, 372)
panel.BackgroundColor3 = Color3.fromRGB(20, 21, 28)
panel.Visible = false
panel.Parent = gui
corner(panel, 12)
local title = label(panel, "Admin panel · " .. tostring(info.rankName), 12)
title.TextColor3 = Color3.new(1, 1, 1)
title.Font = Enum.Font.GothamBold
title.TextSize = 16
label(panel, "Player name, or a user ID to ban or unban", 40)
local targetBox = box(panel, "Name or user ID", 60)
label(panel, "Ban length in minutes (empty = permanent)", 104)
local minutesBox = box(panel, "60", 124)
label(panel, "Reason the player sees", 168)
local reasonIndex = 1
local reasonButton = button(panel, reasons[1], Color3.fromRGB(45, 48, 62), 0, 188, 1)
local kickButton = button(panel, "Kick", Color3.fromRGB(220, 140, 40), 0, 240, 0.5)
local banButton = button(panel, "Ban", Color3.fromRGB(210, 60, 70), 0.5, 240, 0.5)
local unbanButton = button(panel, "Unban user ID", Color3.fromRGB(60, 150, 90), 0, 286, 1)
local status = label(panel, "", 336)
status.TextWrapped = true
status.Size = UDim2.new(1, -32, 0, 30)
toggle.Activated:Connect(function()
panel.Visible = not panel.Visible
end)
reasonButton.Activated:Connect(function()
reasonIndex = reasonIndex % #reasons + 1
reasonButton.Text = reasons[reasonIndex]
end)
local busy = false
local function send(action: string)
if busy then
return
end
busy = true
status.Text = "Working..."
local sent, result = pcall(function()
return remote:InvokeServer({
action = action,
target = targetBox.Text,
reason = reasons[reasonIndex],
minutes = tonumber(minutesBox.Text),
})
end)
status.Text = if sent and type(result) == "table" then tostring(result.message) else "No answer from the server."
busy = false
end
kickButton.Activated:Connect(function()
send("kick")
end)
banButton.Activated:Connect(function()
send("ban")
end)
unbanButton.Activated:Connect(function()
send("unban")
end)
Players without a rank never get the panel, but that is only tidiness: the server would refuse them anyway. The panel builds its frame in code, so there is nothing to set up in StarterGui. To restyle it, change the colours in box and button, or build your own frame and keep the send function.
Step 3: Bans across servers
A popular game runs many servers at once. Without help, a ban only removes the player from the server your admin is in; they would be kicked the next time they join, from the data store, but could keep playing in another server until then.
The fix is MessagingService. Every server subscribes to the AdminBans topic when it starts. When an admin bans someone, the server saves the ban, then publishes the user ID, reason and end time. Every server gets the message, usually within seconds, and kicks that player if they are there.
- If MessagingService is not available, for example in a Studio test of a place that was never published, the script warns "cross-server bans are off" and the rest still works.
- Messages can arrive late or not at all, so the data store stays the source of truth: the join check catches anyone a message missed.
Step 4: Roblox's built-in ban API
Roblox has its own ban API: Players:BanAsync, UnbanAsync and GetBanHistoryAsync. Turn it on with the BanningEnabled property of Players, in Studio. Roblox removes a banned player from every server itself, and by default it also bans their suspected alt accounts.
| This guide's data store bans | Players:BanAsync | |
|---|---|---|
| Removes from every server | Yes, with MessagingService | Yes, by Roblox |
| Alt accounts | No | Yes, unless ExcludeAltAccounts is true |
| Works in a Studio test | Yes, with API access on | Runs, but bans made in Studio do not apply to the live game |
| Message the player sees | Your fixed reasons | DisplayReason, up to 400 characters, filtered by Roblox |
| Where you manage bans | Your panel | Your scripts, or Roblox's Open Cloud API |
To switch, replace the SetAsync and publish in the ban handler with a call like Players:BanAsync({ UserIds = { userId }, Duration = 3600, DisplayReason = reason, PrivateReason = "banned by " .. caller.UserId }), where Duration is in seconds and -1 means permanent. It can only be called from a server script.
Common errors and fixes
If the panel does not show or a button does nothing, check these.
The Admin button never appears
Your user ID is not in RANKS, or the place is owned by a group (the Studio Owner shortcut only works for a place owned by your own account). Check the ID and look for errors in the Output.
"The ban was not saved" in the status line
Data stores are not reachable. In Studio, publish the place and turn on Enable Studio Access to API Services.
"You can only kick players below your rank"
Working as intended: staff cannot act on someone of the same rank or higher. Give the owner rank 3.
Banning by name says no player matches
Names only work for players in your server. For someone offline, type their user ID instead.
A banned player still plays in another server
MessagingService did not deliver, or the place was not published. They are removed the next time they join, from the saved ban.
Build it with RoCode
If you would rather describe the panel than wire it up by hand, RoCode can build it in the place you have open. RoCode is an independent AI agent for Roblox Studio, not made by Roblox: you describe the change in a web chat, and its Studio plugin creates the scripts and GUI in your Explorer.
Add an admin panel for me (owner, user ID 111) and two moderators (222, 333): moderators can kick, I can ban for a set time or for good, every check on the server, bans saved and applied across all servers.
- Writes the server script and the panel with your ranks and the actions you ask for
- Can read your place first and work with an admin script you already have
- Is told to compile-check each script with Check Script, and sets undo points around each batch of changes
You still test it: with a second player for the rank checks, and in a published place for saving and cross-server bans. How RoCode connects to Studio.
Questions
How do I make an admin panel in Roblox?
Keep the ranks and actions in a server Script behind one RemoteFunction, check the caller's rank there on every request, and build the panel in a LocalScript that only shows for staff. Steps 1 and 2 have both scripts.
How do I ban someone from my Roblox game for good?
Save a ban with an end time of -1 in a data store and kick anyone with a saved ban when they join, as this guide does. Or use Players:BanAsync with Duration set to -1, which also bans their alt accounts.
Do I need HD Admin or Adonis?
No. Those admin systems add many commands and a panel, but they are large code you did not write. A short panel of your own does exactly what you need, and you know every line that can kick a player.
Can exploiters use my admin panel?
Not if every check is on the server. A cheater can fire the RemoteFunction, but OnServerInvoke checks their rank by user ID and refuses.
How does a ban work across servers?
The server that bans publishes the user ID on a MessagingService topic. Every other server listens on that topic and kicks the player if they are there.
Sources and further reading
- Roblox Creator Docs: Cross-server messaging. SubscribeAsync and PublishAsync
- Roblox Creator Docs: Players:BanAsync. the built-in ban API and its options
- Roblox Creator Docs: RemoteFunction. OnServerInvoke
- Roblox Creator Docs: Data stores. GetAsync, SetAsync and RemoveAsync
- Roblox Creator Docs: Text filtering. when typed text must be filtered
How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 25 October 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.