Tutorial·2026-10-25·10 min

Roblox Admin Panel Script: Build a Safe Admin GUI in Studio

Make a Roblox admin panel with kick and ban buttons. Copy-paste server script, GUI setup and the official BanAsync API, so exploiters can't use it.

S

Sametcan Tasgiran

Founder & Developer · Picoo

A Roblox admin panel is a GUI that sends commands to the server through a RemoteEvent, and the server checks that the sender is an admin on every call before it does anything. Kicks use Player:Kick on the server, and for bans you can use the official Players:BanAsync API or a DataStore ban list that is checked on PlayerAdded. BanAsync is the recommended option because it also covers suspected alt accounts and removes the player from every running server.

How an admin panel works

There are three parts:

  • The GUI (client). A ScreenGui with a player list, buttons like Kick and Ban, and a reason TextBox.
  • **The RemoteEvent.** One RemoteEvent in ReplicatedStorage, for example AdminCommand, that carries the command from the client to the server.
  • The server Script. A Script in ServerScriptService that checks if the sender is an admin, validates the arguments and runs the command.

The server is the source of truth. It should verify what the client claims before acting on it, as the Roblox remote events docs explain. A LocalScript admin check is easy for exploiters to bypass, so hiding the panel from non-admins is only cosmetic. The real check has to happen on the server when the RemoteEvent fires.

Step by step: build your own admin panel

1. List your admins by UserId

Use UserIds, not usernames, because usernames can change. Your UserId is the number in your profile URL, for example roblox.com/users/123456789/profile.

You can also allow anyone at or above a group rank. On the Player object, both GetRankInGroup and GetRankInGroupAsync are now deprecated. Use [GroupService:GetRolesInGroupAsync(userId, groupId)](https://create.roblox.com/docs/reference/engine/classes/GroupService) instead. It returns a table with IsMember and Roles. Roles is sorted from highest to lowest, and each role has Id, Name and Rank. Results are cached and may be out of date, so a rank change may not show up right away. The docs sample for this method runs on the client, so test it in your server Script before you publish.

2. Create the RemoteEvent

In ReplicatedStorage, insert a RemoteEvent and name it AdminCommand.

3. Build the GUI and LocalScript

Put the ScreenGui in **ServerStorage**, not StarterGui, and name it AdminPanel. Anything in StarterGui is copied to every player, so everyone would see your panel. The server Script in step 4 gives a copy only to admins.

Inside AdminPanel, add a Frame with:

  • a ScrollingFrame named PlayerList with a UIListLayout inside it
  • a TextBox named ReasonBox
  • TextButtons named KickButton and BanButton

Then add a LocalScript inside the ScreenGui:

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local AdminCommand = ReplicatedStorage:WaitForChild("AdminCommand")

local frame = script.Parent:WaitForChild("Frame")
local playerList = frame:WaitForChild("PlayerList")
local reasonBox = frame:WaitForChild("ReasonBox")
local kickButton = frame:WaitForChild("KickButton")
local banButton = frame:WaitForChild("BanButton")

local selectedUserId = nil

local function refreshList(leaving)
    for _, child in playerList:GetChildren() do
        if child:IsA("TextButton") then
            child:Destroy()
        end
    end
    for _, plr in Players:GetPlayers() do
        if plr ~= leaving then
            local button = Instance.new("TextButton")
            button.Size = UDim2.new(1, 0, 0, 30)
            button.Text = plr.Name
            button.Parent = playerList
            button.Activated:Connect(function()
                selectedUserId = plr.UserId
            end)
        end
    end
end

local function send(command)
    if selectedUserId then
        AdminCommand:FireServer(command, selectedUserId, reasonBox.Text)
    end
end

kickButton.Activated:Connect(function() send("Kick") end)
banButton.Activated:Connect(function() send("Ban") end)

Players.PlayerAdded:Connect(function() refreshList() end)
Players.PlayerRemoving:Connect(function(plr)
    if plr.UserId == selectedUserId then
        selectedUserId = nil
    end
    refreshList(plr)
end)
refreshList()

Do not pass the local player to FireServer. A common bug is adding the player as an extra argument, which shifts every other argument by one. The server already receives the firing player as the first parameter.

4. Write the server Script

Put this Script in ServerScriptService. It hands the panel to admins, then checks admin status on every command and validates every argument.

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerStorage = game:GetService("ServerStorage")
local GroupService = game:GetService("GroupService")

local AdminCommand = ReplicatedStorage:WaitForChild("AdminCommand")
local panelTemplate = ServerStorage:WaitForChild("AdminPanel")

local ADMIN_IDS = {
    [123456789] = true, -- replace with your UserId
}
local ADMIN_GROUP_ID = 0 -- set to your group ID to allow group ranks
local ADMIN_MIN_RANK = 250

local function isAdmin(player)
    if ADMIN_IDS[player.UserId] then
        return true
    end
    if ADMIN_GROUP_ID ~= 0 then
        local ok, result = pcall(function()
            return GroupService:GetRolesInGroupAsync(player.UserId, ADMIN_GROUP_ID)
        end)
        return ok and result.IsMember and result.Roles[1] ~= nil
            and result.Roles[1].Rank >= ADMIN_MIN_RANK
    end
    return false
end

local commands = {}

commands.Kick = function(admin, target, reason)
    target:Kick(reason)
end

commands.Ban = function(admin, target, reason)
    local ok, err = pcall(function()
        Players:BanAsync({
            UserIds = {target.UserId},
            Duration = 86400, -- 24 hours, or -1 for permanent
            DisplayReason = reason,
            PrivateReason = "Banned by " .. admin.Name .. " (" .. admin.UserId .. ")",
        })
    end)
    if not ok then
        warn("Ban failed:", err)
    end
end

commands.Bring = function(admin, target)
    local adminRoot = admin.Character and admin.Character:FindFirstChild("HumanoidRootPart")
    local targetCharacter = target.Character
    if adminRoot and targetCharacter then
        targetCharacter:PivotTo(adminRoot.CFrame * CFrame.new(0, 0, -4))
    end
end

AdminCommand.OnServerEvent:Connect(function(player, command, targetUserId, reason)
    if not isAdmin(player) then
        warn(player.Name .. " fired AdminCommand without admin access")
        return
    end
    if type(command) ~= "string" or not commands[command] then
        return
    end
    if type(targetUserId) ~= "number" then
        return
    end
    local target = Players:GetPlayerByUserId(targetUserId)
    if not target then
        return
    end
    if (command == "Kick" or command == "Ban") and isAdmin(target) then
        warn(player.Name .. " tried to " .. command .. " an admin")
        return
    end
    if type(reason) ~= "string" or reason == "" or not utf8.len(reason) then
        reason = "No reason given"
    end
    if #reason > 400 then
        reason = string.sub(reason, 1, utf8.offset(reason, 0, 401) - 1)
    end

    local runCommand = commands[command]
    runCommand(player, target, reason)
end)

local function givePanel(player)
    if isAdmin(player) then
        local panel = panelTemplate:Clone()
        panel.ResetOnSpawn = false
        panel.Parent = player:WaitForChild("PlayerGui")
    end
end

Players.PlayerAdded:Connect(givePanel)
for _, player in Players:GetPlayers() do
    task.spawn(givePanel, player)
end

A few things to notice:

  • The first parameter of OnServerEvent is always the Player who fired it. Roblox fills that in for you, so the server never trusts a player sent inside the payload.
  • Kick and Ban refuse to target any admin. That stops you from kicking yourself with a misclick, and it stops one admin from banning another admin or the owner.
  • The reason is cut to 400 **bytes** on a character boundary. DisplayReason allows 400 characters and a kick message allows 500 bytes, so 400 bytes fits both. Emoji and many non-English letters take more than one byte, which is why the code uses utf8.offset instead of a plain string.sub that could cut a character in half.
  • Giving the panel only to admins is cosmetic. The isAdmin check inside OnServerEvent is the real protection.
  • The commands table keeps each command in one place. Adding a new one means adding one function. Some developers split each command into its own ModuleScript and require it from the same RemoteEvent handler.
  • This panel only targets players who are in the server right now. BanAsync also accepts UserIds of offline players if you add a UserId TextBox.

5. Add more commands on the server

Freeze, walkspeed and heal work the same way: they run on the server after the admin check. For example, a freeze toggle:

commands.Freeze = function(admin, target)
    local root = target.Character and target.Character:FindFirstChild("HumanoidRootPart")
    if root then
        root.Anchored = not root.Anchored
    end
end

Add a FreezeButton and call send("Freeze") from the LocalScript. Fly is the exception: the server only authorizes it, and the client applies the movement.

Banning with BanAsync

[Players:BanAsync](https://create.roblox.com/docs/reference/engine/classes/Players) is the official Ban API. It can only be called from the server, and calling it from the client errors.

Before it works, turn on banning. In Studio, select **Players** in the Explorer and tick **BanningEnabled** in the Properties window. You can't set it from a script. It controls BanAsync, UnbanAsync and GetBanHistoryAsync.

Required fields:

  • UserIds: an array of UserIds, up to 50 per call. It must be a table, so use {userId} even for one person.
  • Duration: whole seconds as a number. Use -1 for permanent. 0 and other negative numbers are invalid. A string here makes the ban fail.
  • DisplayReason: what the banned user sees. Up to 400 characters, and Roblox text-filters it.
  • PrivateReason: notes for you and your team. Up to 1000 characters, not filtered.

Optional fields:

  • ApplyToUniverse: defaults to true, so the ban covers every place in your experience.
  • ExcludeAltAccounts: defaults to false, so the ban spreads to suspected alt accounts unless you set it to true.
  • ApplyDeviceBlock: defaults to false. When true, it blocks the device for 24 hours.

BanAsync also removes the player from every running server right away, so you don't need to call Kick as well.

The official code sample wraps the call in pcall, uses a 24 hour duration and suggests building your own logic based on the user's ban history. You can read that history with Players:GetBanHistoryAsync, but it only works on live servers, not in Studio or on the client.

Unbanning

Use UnbanAsync with the same ApplyToUniverse scope as the original ban, or it will not lift it:

local ok, err = pcall(function()
    Players:UnbanAsync({
        UserIds = {userId},
        ApplyToUniverse = true,
    })
end)
if not ok then
    warn("Unban failed:", err)
end

Only UnbanAsync lifts a device block. Unbanning any other way, such as through Open Cloud or the Creator Hub, does not.

Testing your panel

  • **Check the panel as an admin.** Press Play in Studio. You join with your own account, so if your UserId is in ADMIN_IDS, the panel shows up.
  • **Check that non-admins are blocked.** In Studio, open the Test tab and start **Clients and Servers** with 2 players. Test players are not on your admin list, so they don't get the panel. In one client window, run game.ReplicatedStorage.AdminCommand:FireServer("Kick", game.Players.LocalPlayer.UserId) in the command bar. The server output should show the warning, and nobody should be kicked.
  • Bans in Studio are not real. BanAsync works in Studio and team test, but those bans do not apply to the live game.
  • **Publish for the rest.** Real bans and GetBanHistoryAsync only work on live servers, so test those in a published game with an alt account as the target.

For more on why the server check matters, read the anti-exploit guide.

Using a ready-made admin system

If you want something that works right away:

  • **Adonis** is a community-maintained server moderation system. Insert the official loader model from the Toolbox into ServerScriptService (not Workspace). Configure it in Adonis_Loader > Config > Settings, and change settings.DataStoreKey to a random value first.
  • HD Admin is another popular option. Older guides describe its classic rank setup, but newer versions changed how ranks work, so follow the setup notes that come with the version you insert.

**Be careful in the Toolbox.** Search for the model from its official creator account and check the creator name before inserting. Fake copies can hide backdoors that give strangers admin in your game. If a model has a script with require(<number>) that you didn't add, delete it.

There are also paid panels on sites like BuiltByBit, itch.io and Gumroad. You can't audit them before you buy, so check reviews and read the scripts after inserting.

Generating an admin panel with Picoo

[Picoo](https://picoo.io/) has a dedicated [admin panel builder](/how-to-make/roblox-admin-panel). Type something like "make me an admin panel with kick and ban" and it creates the server Script (AdminServer in ServerScriptService), the client script (AdminClient) and the AdminPanel GUI for you.

  • It has 11 commands: Bring, Go To, Freeze, Heal, God, Speed, Kill, Kick, Ban, Fly (me) and Announce.
  • An isAdmin(plr) check runs on the server before any command. A non-admin who fires the remote gets a warning in the server log and nothing happens.
  • In a user-owned place the owner is an admin automatically. In a group game, list your admins explicitly, since it uses an explicit list and not group ranks. You can name admins by username or UserId. UserId is the reliable option because usernames change.
  • Bans are saved to a DataStore (PicooAdminBans) by default and checked again on PlayerAdded, so a banned player is kicked when they join. In Studio play tests or an unpublished place they only last for the current server. Session-only bans are also an option. It does not use BanAsync, so if you want the official Ban API you would need to rewrite the ban part of AdminServer yourself. The DataStore guide explains how saved data works.
  • The panel opens with F2 by default, and you can pick another letter or F-key. The GUI is built as real instances in Edit mode, so you can restyle it without pressing Play.

It is premium-only and has no free tier. The pricing breakdown covers the Pro and Business plans.

FAQ

Does BanAsync ban alt accounts?

By default, yes. Bans spread to suspected alt accounts unless you set ExcludeAltAccounts = true. One DevForum feature request reports that newly created alts are not caught, but that is a user report, not official documentation.

Is hiding the admin GUI from other players enough?

No. Exploiters can fire your RemoteEvent without ever seeing the GUI. The server has to check isAdmin on every call.

Should I list admins by username or UserId?

UserId. Usernames can change, so a username list can break or give access to the wrong person.

Can I get admin in someone else's game?

No. An admin panel only works in games you build or manage. Scripts that claim to give you admin in other people's games are exploits, and using them breaks Roblox's Terms of Use.

Want to try Picoo?

Premium tool for serious Roblox developers. Pro from $12/mo billed yearly — cancel anytime. Plugin installs in two minutes.

Get Pro →