Why server logic in ReplicatedStorage modules is exposed
Any module that reaches the client can be decompiled, even if it never runs there. How to keep anti-cheat rules and unreleased content on the server.
What it looks like
A ModuleScript in ReplicatedStorage, or anywhere else that replicates, holds server-only logic: a branch behind RunService:IsServer(), or functions that only server scripts call. It might hold anti-cheat limits, reward rules, hidden admin checks or content that isn't released yet. The developer assumes clients never run that code, so they can't see it.
if RunService:IsServer() then function Quests.claim(player, questId) ... end end
How to detect it
- List every ModuleScript in
ReplicatedStorage,ReplicatedFirst,StarterPlayer,StarterGui,StarterPackandWorkspace. - Roblox's access control guide says any ModuleScript that replicates can be decompiled by an exploiter, even if it is never required on the client. It warns against modules that mix server and client code, because the server logic is exposed.
- Look for
RunService:IsServer()orIsClient()branches,OnServerEventconnections,DataStoreService,MessagingServiceorHttpServicecalls, and functions with names likegrant,claim,banorvalidatein those modules. - Look for detection thresholds, secret quest or code lists, release dates and unreleased items in replicated data.
- Secrets such as API keys and webhooks are API keys or webhooks that clients can read, and values sent to clients are Hidden information sent to every client. This check is about logic and rules.
What an exploiter could do
They decompile the module and read exactly how the server checks them. Anti-cheat limits tell them how fast they can move or fire without being caught. Reward rules show which checks are missing. Unreleased content and hidden quests can be leaked or claimed before launch. None of this is a bug on its own, but it makes finding and using real bugs much faster.
How to fix it
- Move server-only code into ModuleScripts in
ServerScriptServiceorServerStorage, which never replicate. - Keep only what the client genuinely needs, such as display names and prices, in shared modules.
- Don't ship unreleased content in replicated places until it's ready to be seen.
Vulnerable example
local RunService = game:GetService("RunService") local Movement = { walkSpeed = 16, } if RunService:IsServer() then Movement.maxSpeed = 24 Movement.strikesBeforeKick = 5 function Movement.tooFast(distance: number, elapsed: number): boolean return elapsed > 0 and distance / elapsed > Movement.maxSpeed end end return Movement
Fixed example
return {
walkSpeed = 16,
}local Shared = require(game:GetService("ReplicatedStorage").Shared.Movement) local SpeedRules = { maxSpeed = Shared.walkSpeed * 1.5, strikesBeforeKick = 5, } function SpeedRules.tooFast(distance: number, elapsed: number): boolean return elapsed > 0 and distance / elapsed > SpeedRules.maxSpeed end return SpeedRules
The first module stays in ReplicatedStorage. The second lives in ServerScriptService.
Common false positives
- Shared modules with only data the client already sees, such as item names, prices shown in the shop or tween settings, are fine.
- Validation the client runs only to give quick feedback, which the server repeats, exposes nothing the client couldn't work out by trying.
- Well-known open-source frameworks whose code is public anyway expose nothing new. Judge the game's own logic inside them.