roblox-architecture
Use when assigning Roblox feature ownership, code location, dependencies, startup, or client-server boundaries without imposing a framework.
Install
npx skills add https://github.com/TabooHarmony/roblox-brain/tree/main/skills/core/roblox-architecture
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tabooharmony-roblox-brain@llmmart
git clone https://github.com/TabooHarmony/roblox-brain.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole tabooharmony/roblox-brain collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Roblox Architecture
When to Load
Load when code ownership is unclear, a feature crosses client and server, startup order matters, or a module is being split. Do not load merely to add a service/controller framework to a small feature.
Quick Reference
Start from one owner
For each behavior, name:
- authoritative state and who may mutate it;
- public operations and callers;
- Roblox instances, connections, and tasks it owns;
- persistence or network boundary;
- startup and teardown conditions.
Group by feature when that keeps one change together. Split server, client, and shared code only where the runtime boundary requires it. Shared code contains no secrets or authoritative mutable state because replicated code is readable by clients.
Use the smallest dependency shape
Direct module calls are the default for a stable dependency. Use a signal when one publisher has genuinely independent observers. Do not add a global event bus, dependency container, manager class, or Init/Start ceremony to hide an ordinary dependency.
Keep module top-level work cheap and non-yielding. A small bootstrap owns only startup that truly needs ordering. Call ordered startup sequentially and fail visibly. Concurrency must be explicit and safe, not automatic task.spawn around every module.
Bound WaitForChild when a dependency arrives through replication and handle
timeout. An unbounded wait turns a missing instance into a silent startup hang.
Enforce runtime authority
The client presents input and prediction. The server validates and decides authoritative outcomes. Remotes are APIs with types, bounds, state, ownership, abuse controls, and failure behavior. Route implementation details to roblox-networking and roblox-security.
Split only for evidence
Split when there is a separate lifecycle or authority boundary, a distinct persistence contract, an independently testable pure core, or unrelated reasons to change. Do not split for folder symmetry or speculative reuse.
Review
One canonical owner per mutation, no hidden startup yield or replicated trust decision, explicit cleanup, and the smallest traceable structure. For tag-driven behavior, use tags for discovery, attributes for configuration, and one owner for attach/remove cleanup.
Detailed layouts, dependency rules, and startup examples: references/full.md
Files (roblox-brain)
-
references
-
full.md 14.1 KB
# Roblox Architecture: Full Reference Architecture is the ownership and dependency model that makes changes safe. Folders and class names are evidence of that model only when they clarify a real boundary. ## 1. Define the feature before the hierarchy For a behavior, answer: 1. Which state is authoritative? 2. Which code may mutate it? 3. Which operations are public, and who calls them? 4. Which instances, connections, tasks, and cached values does it own? 5. Does it cross a client/server, persistence, purchase, or external-service boundary? 6. When does it start, and how does it stop? If those answers fit in one small module or script, keep them there. A service/controller pair is not automatically more architectural than two scripts. Add boundaries because ownership differs, not because a template has folders to fill. ## 2. Runtime location is a trust and execution decision | Location | Typical role | Important consequence | | --- | --- | --- | | `ServerScriptService` | server rules and orchestration | not replicated to clients | | `ServerStorage` | server-only templates and assets | unavailable to clients | | `ReplicatedStorage` | shared definitions, modules, and remotes | readable by clients | | `StarterPlayerScripts` | per-player client behavior | cloned and run for each player | | `StarterGui` | UI templates | cloned into each player's GUI | | `Workspace` | live world instances | replicated according to engine behavior | Replicated code is not a secret store. Do not put credentials, private reward logic, hidden detection thresholds, or authoritative mutable state in replicated locations and assume clients cannot inspect it. A shared module may hold types, immutable identifiers, pure calculations, or presentation-safe configuration. The server still owns decisions that grant value, change persistent state, charge a purchase, or affect other players. ## 3. Prefer feature cohesion over ceremonial layers A small feature can keep related code together while respecting runtime boundaries: ```text ReplicatedStorage/Features/Inventory/ ├── Types.luau └── Remotes/ ServerScriptService/Features/Inventory/ ├── Inventory.luau └── Inventory.server.luau StarterPlayer/StarterPlayerScripts/Features/Inventory/ ├── InventoryView.luau └── Inventory.client.luau ``` This is an example, not a required tree. A flat layout is better when the project is small. A top-level server/shared/client layout is better when that is already consistent. Do not reorganize a working repository solely to match this specimen. The important properties are: - one obvious server owner for authoritative mutation; - client code owns input and presentation, not grants; - shared code is safe to replicate; - one feature change does not require hunting through unrelated generic manager folders. ## 4. Module contracts A useful module contract states its owned state, public operations, caller side, failure modes, and lifecycle. ```luau local Inventory = {} local quantities: {[Player]: {[string]: number}} = {} function Inventory.getCount(player: Player, itemId: string): number local playerItems = quantities[player] return if playerItems then playerItems[itemId] or 0 else 0 end function Inventory.remove(player: Player, itemId: string, amount: number): boolean if amount < 1 then return false end local playerItems = quantities[player] local current = if playerItems then playerItems[itemId] else nil if current == nil or current < amount then return false end playerItems[itemId] = current - amount return true end function Inventory.release(player: Player) quantities[player] = nil end return Inventory ``` This does not need a class, dependency container, base service, or lifecycle interface. Add one only when repeated concrete behavior pays for it. Keep top-level module code cheap and non-yielding. Hidden work during `require` makes ordering, failure, and cycles hard to diagnose. Circular requires indicate ownership or dependency direction is unclear; do not solve them with delayed globals. ## 5. Dependency direction Use a direct module call when one owner needs a stable operation from another. The dependency remains visible and searchable. Use a signal when: - the publisher should not know independent observers; - zero, one, or several observers are legitimate; - delayed notification is acceptable; - the signal has an owner and cleanup contract. Do not introduce a global event bus merely to erase dependency arrows. It replaces compile-time/searchable relationships with string names, runtime ordering, and hidden consumers. Avoid bidirectional feature dependencies. Move a narrow pure contract downward, let one side own orchestration, or emit an event from the authoritative owner. Shared folders should not become dumping grounds for anything imported twice. ## 6. Startup only when startup exists Many modules need no initialization. Requiring them and calling their operations is enough. When startup order matters, one small bootstrap should make it explicit: ```luau local DataOwner = require(script.Parent.DataOwner) local Match = require(script.Parent.Match) local ok, problem = DataOwner.start() if not ok then error(`Data startup failed: {problem}`) end Match.start(DataOwner) ``` Sequential calls preserve order and surface failure. Do not wrap every `Start` in `task.spawn`; that discards ordering and creates unowned background failures. Run independent startup concurrently only when independence is proven and failures are still collected. Avoid universal two-phase `Init`/`Start` contracts. They add ceremony and can leave modules half-initialized. If phases are necessary, state exactly what each phase guarantees, validate dependency availability, and prevent public operations before readiness. Top-level scripts can wire small features directly. A framework is not required to make startup explicit. If a client dependency arrives through replication, use a bounded `WaitForChild` and handle timeout explicitly. An unbounded wait converts a missing instance or placement mistake into a silent startup hang. ## 7. Client/server APIs A client request is untrusted input, not a command. The server validates the request against current server-owned state before side effects. For each client-to-server remote, define: - payload types and size limits; - finite number and range checks; - current-state preconditions; - player ownership or permission; - replay and duplicate semantics; - abusive-frequency controls based on operation cost; - success, denial, timeout, and reconciliation behavior. Client prediction may improve responsiveness, but it must reconcile with the authoritative result. Never accept client-supplied damage, price, ownership, reward, inventory, or privileged destination as truth. Load `roblox-networking` and `roblox-security` for executable patterns. Load `roblox-monetization` for purchase ownership and `roblox-data` for persistence ownership. ## 8. One canonical side-effect owner Some operations tolerate only one owner: - profile load, save, migration, and session release; - purchase receipt processing and durable grants; - authoritative currency or inventory mutation; - cross-server message deduplication; - external webhook side effects. Feature modules may request these operations. They should not each implement their own save loop, receipt callback, retry policy, or shutdown handler. Duplicate owners create overwrite races and ambiguous recovery. Player removal is a signal, not a universal persistence architecture. The canonical data owner defines leave, crash, teleport, and shutdown behavior. Other features release only the memory and resources they own. ### Player Lifecycle Wiring Wire the ordered player lifecycle once in a single server entrypoint: `PlayerAdded` (load profile) → `CharacterAdded` → `CharacterAppearanceLoaded` (respawn/accessories) → `Humanoid.Died` → `PlayerRemoving` (release profile), and call the join handler on each pre-existing `Players:GetPlayers()` player so none is missed. Gate per-player data sends on a client-ready handshake: the client registers all remote listeners first, then fires a `ClientLoaded`-style signal; the server withholds player-owned payloads until it arrives (and handles the profile-not-yet-loaded race by waiting on that signal). [Community lead: "How to script a game server from scratch" by NullThornException, https://devforum.roblox.com/t/how-to-script-a-game-server-from-scratch-from-a-senior-engineer-tutorial/4741682; label as practitioner design.] ## 9. Split and merge criteria Split a module when at least one is true: - authority changes, such as client presentation versus server decision; - state has a separate lifecycle or persistence contract; - a pure calculation can be tested without Roblox wiring; - dependencies and reasons to change are genuinely unrelated; - resource ownership becomes clearer after the split. Merge or delete a boundary when: - it only forwards calls without policy or translation; - its name is generic but its state belongs to one feature; - an interface has one implementation and no independent contract; - two modules mutate the same state; - boilerplate exceeds the behavior it protects. Do not build speculative plugin systems or factories for one implementation. ## 10. Architecture review Check the real call and data flow, not just folder names. For a player-facing change, trace input → UI/world feedback → remote or simulation → authoritative state → persistence → cleanup. Co-occurring classes or files do not prove a runtime connection. Separate search-only suspicions from observed runtime behavior and verified tests. - Every authoritative state mutation has one canonical owner. - Shared code and instances are safe for clients to read. - Runtime location matches execution and trust requirements. - Public module APIs are narrow and failure behavior is explicit. - No circular require, hidden top-level yield, or accidental concurrent startup. - Direct dependencies remain visible; signals have a real decoupling reason. - Connections, tasks, instances, and player-keyed caches have cleanup owners. - Persistence and purchase callbacks are not duplicated across features. - Tests can isolate pure behavior where doing so is useful. - No service/controller/manager/framework layer exists only for symmetry. - The structure is the smallest one that a new contributor can trace end to end. ## 11. Tag-Driven Composition Use `CollectionService` tags to select instances that receive a behavior, and attributes to hold per-instance configuration. Keep the behavior owner explicit; tags are discovery, not authority. ```luau local CollectionService = game:GetService("CollectionService") local function attach(instance: Instance) -- Make this idempotent and register one cleanup owner. end for _, instance in CollectionService:GetTagged("DamageZone") do attach(instance) end CollectionService:GetInstanceAddedSignal("DamageZone"):Connect(attach) CollectionService:GetInstanceRemovedSignal("DamageZone"):Connect(function(instance) -- Release behavior, connections, and temporary state. end) ``` Initialize existing and future tagged instances. Treat added and removed signals as lifecycle boundaries, including client streaming. Validate attribute types and defaults, keep durable or security-sensitive state server-owned, and make attach/cleanup safe to repeat. ## Community ecosystem (leads, not sources) Open-source study codebases ranked by DevForum likes. Read before architecting similar genres: - [Miner's Haven](https://devforum.roblox.com/t/miners-haven-open-sourced-everything-you-need-to-make-your-own-factory-game/350767): factory/sim systems at scale. - [Ruddev's Battle Royale](https://devforum.roblox.com/t/os-game-ruddevs-battle-royale-open-sourced/340548): full OS game. - [Mass Uncopylocked](https://devforum.roblox.com/t/mass-uncopylocked-35-free-games-and-projects/2880269): 35 open-sourced projects (466k views). - FPS architecture: [Writing an FPS framework](https://devforum.roblox.com/t/writing-an-fps-framework-2020/503318) series remains the most-cited framework-design walkthrough. ## ECS on Roblox: reality check ECS is not standard practice in shipped Roblox experiences. The pattern recurs for specific problems, not as a default architecture. - If a project already uses one, the leading Luau library is [jecs](https://github.com/Ukendio/jecs) (462 stars, active 2026; entity relationships as first-class). Alternatives: matter-ecs (stalled since 2024), ECX (dead, author moved on). - What most production games actually use: OOP tables + CollectionService tags + attribute replication + per-system update loops with rotating work cursors. That combination delivers most of the cache/iteration benefit without the discipline cost. - The honest case for ECS: thousands of homogeneous simulated entities (swarms, RTS units, bullets-with-state). Outside that, the abstraction tax outweighs the gain, and agent-written ECS code adds indirection without the perf need. - Do not recommend introducing ECS to a project that lacks one; do support projects that have one. ## Community field notes (Tizzy discord, Jul–Sep 2026) Practitioner reports from a live dev Discord; repeated field observations, not doc-verified claims. <!-- temporal: 2026-09 --> - **Module-loader race pattern:** race conditions from dependency chains ("require this → require that" dominoes) cause playtests that only work after 2–3 restarts. Fix pattern: server services and client controllers don't depend on each other until their `.Init()` is called; a module loader requires all modules, then initializes all (optionally `task.spawn` per init for parallel init). Also makes the codebase far easier for AI agents to work with. Attribution: BuilderbeastYT/Ibra 2026-07-29; RBobloxian5542 2026-08-07 - **BigNum handling for simulators:** `IntValue` breaks past ~10 quadrillion; `NumberValue` extends only to ~9.22e18; the standard fix is coefficient + exponent pairs (a BigNum-style library, e.g. the "infinitemath" module). Attribution: GameForgeX; renik01; adhx5; Skardoll; Davide, 2026-07-06 - Max server size is 200 players; a "10k player server" is 10k spread across 200-slot servers. Attribution: thug, 2026-08-22
-
-
SKILL.md 2.9 KB
--- name: roblox-architecture description: "Use when assigning Roblox feature ownership, code location, dependencies, startup, or client-server boundaries without imposing a framework." last_reviewed: 2026-08-31 sources: - https://create.roblox.com/docs/projects/data-model - https://create.roblox.com/docs/projects/client-server - https://create.roblox.com/docs/scripting/locations - https://create.roblox.com/docs/scripting/security/access-control - https://create.roblox.com/docs/reference/engine/classes/CollectionService - original --- # Roblox Architecture ## When to Load Load when code ownership is unclear, a feature crosses client and server, startup order matters, or a module is being split. Do not load merely to add a service/controller framework to a small feature. ## Quick Reference ### Start from one owner For each behavior, name: - authoritative state and who may mutate it; - public operations and callers; - Roblox instances, connections, and tasks it owns; - persistence or network boundary; - startup and teardown conditions. Group by feature when that keeps one change together. Split server, client, and shared code only where the runtime boundary requires it. Shared code contains no secrets or authoritative mutable state because replicated code is readable by clients. ### Use the smallest dependency shape Direct module calls are the default for a stable dependency. Use a signal when one publisher has genuinely independent observers. Do not add a global event bus, dependency container, manager class, or `Init`/`Start` ceremony to hide an ordinary dependency. Keep module top-level work cheap and non-yielding. A small bootstrap owns only startup that truly needs ordering. Call ordered startup sequentially and fail visibly. Concurrency must be explicit and safe, not automatic `task.spawn` around every module. Bound `WaitForChild` when a dependency arrives through replication and handle timeout. An unbounded wait turns a missing instance into a silent startup hang. ### Enforce runtime authority The client presents input and prediction. The server validates and decides authoritative outcomes. Remotes are APIs with types, bounds, state, ownership, abuse controls, and failure behavior. Route implementation details to `roblox-networking` and `roblox-security`. ### Split only for evidence Split when there is a separate lifecycle or authority boundary, a distinct persistence contract, an independently testable pure core, or unrelated reasons to change. Do not split for folder symmetry or speculative reuse. ### Review One canonical owner per mutation, no hidden startup yield or replicated trust decision, explicit cleanup, and the smallest traceable structure. For tag-driven behavior, use tags for discovery, attributes for configuration, and one owner for attach/remove cleanup. > Detailed layouts, dependency rules, and startup examples: [references/full.md](references/full.md)
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.