Roblox project structure and client/server model - replication, where code and assets belong (ServerScriptService, ReplicatedStorage, StarterPlayer), RunContext, service/controller bootstrapping, player/character lifecycle, CollectionService components, attributes, streaming-safe client code, library choices. Use when starting a project, deciding where code goes, or fixing replication/streaming bugs.
Installs into .claude/skills of the current project.
Are you the author of Roblox Architecture?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/el4cteo-roblox-architecture)
---
name: roblox-architecture
description: Roblox project structure and client/server model - replication, where code and assets belong (ServerScriptService, ReplicatedStorage, StarterPlayer), RunContext, service/controller bootstrapping, player/character lifecycle, CollectionService components, attributes, streaming-safe client code, library choices. Use when starting a project, deciding where code goes, or fixing replication/streaming bugs.
---
# Roblox game architecture
## The model in one paragraph
Every Roblox game is a **server** (authoritative, runs `Script`s, sees everything, owns DataStores)
plus one **client** per player (runs `LocalScript`s / client `Script`s, renders, reads input).
The server's DataModel **replicates** to clients automatically. Client changes do **not** replicate
back, except: physics of parts the client has network ownership of (its character by default),
its own `Humanoid` state/animations, and remote calls. Assume every client is compromised
(see `roblox-security`). Design every feature as: *client requests → server validates and
decides → state replicates → clients render*.
## Where things go
| Container | Runs/visible on | Put here |
| --- | --- | --- |
| `ServerScriptService` | Server only | Server `Script`s and server-only ModuleScripts (game logic, data, anti-cheat). |
| `ServerStorage` | Server only | Server-only assets: maps, NPC templates, tools to clone, secrets-adjacent config. |
| `ReplicatedStorage` | Both | Shared modules (types, config, pure utils), `RemoteEvent`s, assets clients clone. |
| `ReplicatedFirst` | Client, first | Loading screen only. Replicates before everything else. |
| `StarterPlayer.StarterPlayerScripts` | Client (copied once to `PlayerScripts`) | Client entry point and controllers. |
| `StarterPlayer.StarterCharacterScripts` | Client (copied per spawn into character) | Per-character client scripts (rarely needed). |
| `StarterGui` | Client (copied to `PlayerGui`) | `ScreenGui`s. Set `ResetOnSpawn = false` unless you want reset on death. |
| `Workspace` | Both (streamed to clients) | The 3D world only. Not a place for code or templates. |
| `Lighting`, `SoundService`, `Teams`, `TextChatService` | Both | Their respective config objects. |
Rules:
- Code in `ReplicatedStorage` is **downloadable by exploiters**. Never put server logic, secrets,
admin lists, or anti-cheat thresholds there.
- `Script.RunContext`: `Legacy` (default: behavior depends on the container), `Server`, `Client`, or
`Plugin`. A non-Legacy RunContext makes a `Script` run **regardless of container** (e.g. a `Client`
Script in `ReplicatedStorage` runs on every client) — so templates you clone must not contain such
scripts unintentionally. Legacy `Script`s don't run in `ReplicatedStorage`/`ServerStorage`.
## Project layout (Rojo-style, recommended)
```text
src/
server/ -> ServerScriptService.Server (init.server.luau = entry point)
Services/ -> one ModuleScript per system (DataService, CombatService, ...)
client/ -> StarterPlayer.StarterPlayerScripts.Client (init.client.luau)
Controllers/ -> one ModuleScript per client system (InputController, UIController, ...)
shared/ -> ReplicatedStorage.Shared (types, config, pure logic, network definitions)
Packages/ -> ReplicatedStorage.Packages (Wally/pesde dependencies)
ServerPackages/ -> ServerScriptService.ServerPackages
```
One entry script per side, everything else is a ModuleScript. This gives deterministic load order,
testable modules, and a single place to wire dependencies. Roblox's own recommendation is equivalent:
one `Script` (`RunContext = Server`) in `ServerScriptService` and one `Script` (`RunContext = Client`) in
`ReplicatedStorage`, each requiring modules and calling their `start()`. Tooling setup: `roblox-tooling`.
## Bootstrapping services (no framework needed)
```luau
--!strict
-- ServerScriptService/Server/init.server.luau
type Service = {
init: ((self: Service) -> ())?, -- sync setup; may reference other services; must not yield
start: ((self: Service) -> ())?, -- runs after every init; may yield / connect events
}
local servicesFolder = script:WaitForChild("Services")
local services: { [string]: Service } = {}
for _, module in servicesFolder:GetChildren() do
if module:IsA("ModuleScript") then
services[module.Name] = (require :: any)(module)
end
end
for name, service in services do
if service.init then
debug.setmemorycategory(name)
service:init()
debug.resetmemorycategory()
end
end
for name, service in services do
if service.start then
task.spawn(function()
debug.setmemorycategory(name)
service:start()
end)
end
end
```
- Services reference each other with plain `require` (no cycles) or receive dependencies in `init`.
- The client mirrors this with `Controllers`. Keep names symmetrical (`ShopService` / `ShopController`).
- Frameworks like Knit are unnecessary for this; if a project already uses one, follow its conventions.
## Player and character lifecycle
```luau
--!strict
local Players = game:GetService("Players")
local function onCharacterAdded(player: Player, character: Model)
local humanoid = character:WaitForChild("Humanoid") :: Humanoid
humanoid.Died:Once(function()
print(`{player.Name} died`)
end)
end
local function onPlayerAdded(player: Player)
-- Load data first (see roblox-data-stores), then set up character handling.
if player.Character then
task.spawn(onCharacterAdded, player, player.Character)
end
player.CharacterAdded:Connect(function(character)
onCharacterAdded(player, character)
end)
end
local function onPlayerRemoving(player: Player)
-- Save data, release session locks, clear every table keyed by this player.
print(`{player.Name} left`)
end
Players.PlayerAdded:Connect(onPlayerAdded)
Players.PlayerRemoving:Connect(onPlayerRemoving)
for _, player in Players:GetPlayers() do -- players who joined before this script connected
task.spawn(onPlayerAdded, player)
end
game:BindToClose(function()
-- Server shutdown: flush saves for remaining players (max ~30 s total).
end)
```
- Always handle players/characters that already exist when your code starts.
- Characters respawn: anything bound to a character must be re-created on `CharacterAdded`.
- On the client, character descendants may not exist yet (streaming): `WaitForChild` them.
## Components with CollectionService tags
Tag instances (Studio **Tags** section or `CollectionService:AddTag`) and attach behavior in code.
This scales better than copying scripts into every model and works with streaming because the
added/removed signals fire as instances stream in and out.
```luau
--!strict
local CollectionService = game:GetService("CollectionService")
local TAG = "KillBrick"
local cleanups: { [Instance]: () -> () } = {}
local function onAdded(instance: Instance)
if not instance:IsA("BasePart") then
return
end
local connection = instance.Touched:Connect(function(hit)
local humanoid = hit.Parent and hit.Parent:FindFirstChildOfClass("Humanoid")
if humanoid then
humanoid.Health = 0
end
end)
cleanups[instance] = function()
connection:Disconnect()
end
end
local function onRemoved(instance: Instance)
local cleanup = cleanups[instance]
if cleanup then
cleanup()
cleanups[instance] = nil
end
end
CollectionService:GetInstanceAddedSignal(TAG):Connect(onAdded)
CollectionService:GetInstanceRemovedSignal(TAG):Connect(onRemoved)
for _, instance in CollectionService:GetTagged(TAG) do
task.spawn(onAdded, instance)
end
```
Configure components with **attributes** (`instance:GetAttribute("Damage")`,
`GetAttributeChangedSignal`) instead of `IntValue`/`StringValue` children. Attributes replicate
server → client, are cheaper, and show in the Properties panel.
## Sharing state
| Need | Use |
| --- | --- |
| Small per-instance/per-player values visible to clients | Attributes on the instance / `Player` (server sets). |
| Events and requests | `RemoteEvent` / `UnreliableRemoteEvent` (see `roblox-networking`). |
| Large or per-player private state (inventory, quests) | Server-owned tables + remotes that send snapshots/diffs to that player only. |
| Same-side communication | Module function calls or a typed Signal module — not `BindableEvent`, `_G`, or `shared`. |
| Persistent data | `roblox-data-stores`. Cross-server: MessagingService / MemoryStore. |
## Streaming (on by default for new places)
With `Workspace.StreamingEnabled`, clients only have nearby parts. Client code must:
- `WaitForChild` (with a timeout when it may never arrive) or nil-check anything in `Workspace`.
- Use `ModelStreamingMode = Atomic` for models whose parts scripts need together;
`Persistent` only when a model must always be present (costs memory).
- Treat `GetChildren`, raycasts, and property reads of streamed-out parts as partial/stale; do full-world
queries on the server.
- Pre-stream destinations on the server with `player:RequestStreamAroundAsync(position)` before teleporting a character.
Full pattern catalog and recommended Workspace settings: [references/streaming.md](references/streaming.md).
## Anti-patterns
- Dozens of loose `Script`s inside parts/models (use tags + one service).
- Polling loops (`while task.wait(0.1) do check() end`) instead of events or `Heartbeat` with `deltaTime`.
- Trusting the client for game state, or firing remotes every frame.
- `_G`/`shared` globals, circular requires, yielding at module top level.
- Cloning maps into `Workspace` from the client (client-only copies never get server updates).
- Storing per-player data in the character (it's destroyed on death).
Library ecosystem (signals, cleanup, networking, UI, ECS, data): [references/ecosystem.md](references/ecosystem.md).
## Related skills
`roblox-networking`, `roblox-security`, `roblox-data-stores`, `roblox-tooling`, `roblox-performance`, `roblox-game-systems`.