| name | roblox-architecture-contract |
| description | Use when writing or reviewing code in a firebit house-stack Roblox project and the system's shape matters — service/controller boot order, where a new service goes, require cycles, "attempt to require a module that is loading", profile data not replicating, client sees stale state, choosing action networking vs Replica vs Attributes, hardcoded product/badge IDs, spawn ownership, persisted-schema key renames, or dormant flag-gated code that looks deletable. |
Roblox Architecture Contract (firebit house stack)
Overview
Every firebit game project shares one architecture: Rojo + Wally + strict Luau, Bootstrap convention loading, ProfileDB/Replica persistence behind a single write choke point, red action networking, and rblxsync-generated monetization IDs. This skill is the list of load-bearing design decisions in that stack, WHY each exists, the invariants that must keep holding in the current project, and how to audit a project's weak points. Treat every decision here as intentional until you have evidence otherwise — most encode a past failure.
All commands below run from the current project's repo root and use house-structure paths. Before relying on any specific fact (a count, a file, a flag value), re-verify it in THIS repo with the given one-liner — never carry a number over from another project.
When to use
- Before adding a service, controller, remote, data field, or replication path.
- When a change touches boot order, networking, profile data, spawning, monetization IDs, or a core game loop.
- When code looks wrong ("misspelled key", "dead engine behind a flag", "dev flag always on") and you are tempted to fix it.
When NOT to use
- Deciding whether an action is allowed at all (push, rblxsync run, deleting legacy code, tuning numbers) → roblox-change-control owns the four hard rules.
- Per-flag catalog and how to add a config axis → roblox-config-and-flags.
- Bootstrap/toolchain/sourcemap problems → roblox-build-and-env.
- Roblox platform theory (why Attributes replicate, what ReplicatedFirst is) → roblox-platform-reference.
- Symptom-driven bug triage → roblox-debugging-playbook. History of why decisions were fought over → roblox-failure-archaeology.
Boot narrative (60 seconds)
Jargon: Rojo syncs the filesystem into a Roblox place; Wally is the package manager (deps land in Packages/); Luau is Roblox's typed Lua; red is an action-based networking package; ProfileDB/Replica are the persistence and server→client data-sync packages.
Server: src/Server/Entry.server.luau runs Attrify.start() (attribute/tag-driven behavior watchers), then Bootstrap.initServices(...) and Bootstrap.startServices(...). Client: src/Client/Entry.client.luau does the mirror with controllers. src/Shared/Class/Bootstrap.luau finds every ModuleScript whose name contains "Service" (or "Controller"), requires it in a task.spawn, and calls optional init() then start(). Consequences:
- Naming IS registration. A file named
FooService.luau under src/Server/Features/ boots automatically; nothing else wires it up. A helper module accidentally named *Service will also be required and booted.
- Each init/start runs in its own
task.spawn — there is no ordering guarantee between services. Cross-service dependencies must be resolved lazily (require inside start()/first call) or by injection (decision 2 below).
Each game's loop (day cycles, rounds, waves, whatever the game runs on) is its own set of services under src/Server/Features/. Read those services directly — this skill covers the contract they all obey, not any one game's loop.
Load-bearing decisions (house stack — verify each in the current repo)
| # | Decision | Where (house-standard location) | Why |
|---|
| 1 | Convention-based boot: name-contains-Service/Controller, optional init/start, each in task.spawn | src/Shared/Class/Bootstrap.luau | Zero-registry wiring; adding a feature = adding a correctly named file |
| 2 | Provider injection instead of back-requires: when service A drives service B, A injects a callback/provider into B; B never requires A back | grep Provider setters across src/Server | Strict-mode Luau hard-fails on module require cycles at boot ("attempt to require a module that is loading"). Case study (shipped firebit game, 2026): the day-cycle service injects setCurrentNightProvider / setCurrentPhaseProvider into the wave and NPC services, with a comment stating the cycle rationale |
| 3 | ProfileService is the single profile-write choke point; out-of-band mutations MUST call ProfileService.update(player, updatedKeys) | src/Server/Features/Core/ProfileService.luau docstring declares it | Replica only replicates what you tell it; a silent direct write leaves the client stale forever |
| 4 | update() replication is coarse: it splits each dotted path but replicates the whole top-level key subtree via replica:Set(firstKey, value), then fires the profile-updated signal (and syncs leaderstats for stat paths) | ProfileService.update in the same file | Coarse-grained by design — pass "season.xp" and the entire season table re-replicates. Size your top-level keys accordingly |
| 5 | red action networking; server receives only via handler modules mounted with red.useHandlers(script.Parent.Handlers); per-handler server-side validation; red.bind sites are rare and deliberate | grep both patterns (invariants table below) | One networking idiom; validation lives next to the action it guards; no NEW raw RemoteEvents in src/ — enumerate and pin your project's sanctioned exceptions |
| 6 | Three replication channels, each with a correct use (table below) |
All projects mount src/Server → ServerScriptService, src/Client → ReplicatedFirst (not StarterPlayerScripts — client boots before replication finishes, hence the explicit loading screen in Entry.client.luau), src/Shared → ReplicatedStorage.Shared, Packages → ReplicatedStorage.Packages.
The three replication channels — pick correctly
| Channel | Direction | Use for | Example |
|---|
red actions (red.dispatch / handler modules) | both ways, event-shaped | Commands and one-shot events: purchases, phase broadcasts, claims | a phase-change broadcast, a revive request |
Replica (profile replica, class "Profile") | server→client, state-shaped | Persistent profile data the UI renders | the client data controller subscribes; UI reads client state |
| Instance Attributes | server→client, per-instance flags | Cheap per-instance state where a red round-trip is overkill; client reacts via GetAttributeChangedSignal | a lock flag on a vehicle, a subscription flag on the Player |
Do not add NEW raw RemoteEvents — red owns command networking. Enumerate your project's sanctioned Instance.new("RemoteEvent") sites once (grep -rn 'Instance.new("Remote' src --include="*.luau"), understand why each exists, and treat any change to that list as a review item. Case study (shipped firebit game, 2026): one sanctioned remote was a deliberate anti-exploit honeypot with an innocuous name — deleting "unused" remotes without reading them destroys traps like that.
Invariants — each with its proof command
Run from the current project's repo root. If a check fails, either the invariant broke (treat as a bug) or this project deviates from the house stack (confirm with the owner and record it).
| Invariant | One-line verification |
|---|
red.bind sites are few and known — pin the list | grep -rn "red.bind(" src --include="*.luau" |
Server handlers all mount via useHandlers | grep -rln "red.useHandlers" src/Server |
| No unsanctioned raw remotes in src | grep -rn 'Instance.new("Remote' src --include="*.luau" → matches your pinned list |
| ProfileService is still the declared write choke point | sed -n '1,10p' src/Server/Features/Core/ProfileService.luau |
| Inventory wipe on load AND save while SaveInventory=false | grep -n "SAVE_INVENTORY|SaveInventory" src/Server/Features/Core/ProfileService.luau src/Shared/Class/FeatureFlags.luau |
| Session currency never touches the profile | grep the currency's storage in its service — in-memory table, no profile writes |
| CharacterAutoLoads=false in shipping place projects (if project owns spawning) | grep -n "CharacterAutoLoads" *.project.json |
| Spawning only via tagged parts | grep the spawn-tag constant across src/Server and src/Shared/Data |
| Server re-checks gameplay gates in handlers | grep the gate attribute name in the owning service and its handlers |
| Provider injection still in place (no back-require of the driving service) | grep the provider setters, then grep the driving service's name in the driven services → only type/comment hits |
| Generated.luau is machine-owned | head -3 src/Shared/Data/Monetization/Generated.luau |
| Terminal-state fan-out is idempotent | read the game-state service's end-game function: guard bool + single signal |
| Strict mode global | cat .luaurc |
Auditing a project's weak points
The reference implementation had six standing weak points; every house-stack project should be audited for the same classes. Run these and write the findings into the project's docs/ARCHITECTURE.md (doc-debt ledger: roblox-docs-and-writing).
- Test coverage gaps. Services are usually verified only by playtests.
find src -name "*.spec.luau" — if the list is nearly empty, say so plainly when asked about confidence. Evidence bar and test conventions: roblox-validation-and-qa.
- Doc drift on renames. Renamed Rojo project files and moved modules leave stale references in
docs/ARCHITECTURE.md. Trust the repo root over the doc; verify with git log --diff-filter=R --name-status --oneline -- "*.project.json" and grep the doc for old names. Learned in a shipped firebit game, 2026: the architecture doc referenced a build project file that had been renamed months earlier.
- Security hardening status. Check
docs/plans/ for a security-hardening plan and count its unchecked boxes (grep -c "\[ \]" docs/plans/<plan>.md). Until server-side validation of damage, range, ownership, payload types, and fire rate is DONE (not planned), assume a hostile client can exploit the weapon/combat path.
- Dev-flag booby traps. Any "is this production?" derivation from a place-ID constant is suspect: if the constant is a placeholder (e.g.
productionPlaceId = 0), the flag evaluates wrong everywhere. Grep who consumes it before trusting it. Learned in a shipped firebit game, 2026: ServerConfig.dev evaluated true even in production because the production place ID was never filled in — harmless only because nothing consumed it yet. Details: roblox-config-and-flags; changing such a constant needs owner sign-off per roblox-change-control.
- Canonical misspellings in persisted schemas. Do NOT fix them. A misspelled key in the DataTemplate/profile types is a key in every existing player's DataStore profile; renaming it orphans their data unless you write a migration. Spell it wrong, consistently. Find them: grep suspicious keys in
src/Shared/Data/Core/DataTemplate.luau and the profile types file, and check both agree. Learned in a shipped firebit game, 2026: proccessed_passes [sic] lives in production profiles and must stay misspelled.
- Dormant flag-gated engines. Whole subsystems (a round engine, a legacy mode) may be flag-gated off in FeatureFlags and deliberately kept. They are NOT the live game loop and NOT dead code — deletion is forbidden, see roblox-change-control. Learned in a shipped firebit game, 2026: a lobby minigame round base class was repeatedly misread as the main survival loop; the docstring, not the name, says what a class is for.
Common mistakes
- Naming a plain helper module
*Service/*Controller — Bootstrap will require and boot it.
- Requiring another service at module top level and assuming it initialized first — there is no boot order; require lazily or inject a provider (decision 2 is the template).
- Mutating profile data directly and forgetting
ProfileService.update() — the server is right, the client is stale, and nothing errors.
- Persisting run-scoped progression as inventory items or session currency — it evaporates (
SaveInventory=false, session currency is in-memory). Persistent rewards go through persisted currencies/XP only.
- Hand-editing
Monetization/Generated.luau or hardcoding a freshly minted DevProduct ID — regenerated/reissued on the next rblxsync run. Only hand-paste IDs where your project has a sanctioned data-module exception (verify first).
- Gating purchases, entry, or revives on the client only — every gate is server-side first (decisions 5, 9); the client copy is cosmetic.
- Using
Instance.new for UI or adding a raw RemoteEvent — React-only UI (see luau-react) and red-only networking.
- "Cleaning up" misspelled persisted keys or dormant flag-gated subsystems.
- Trusting a derived dev/production flag without reading how its place-ID constant is set.
Provenance and maintenance
Derived 2026-07-05 from the firebit house-stack reference project; every decision and command was verified against that repo's working tree that day. Case studies are attributed and illustrative only — no instruction here depends on that repo or its git history existing.
Re-verification commands run against the CURRENT project repo, from its root:
grep -rn "red.bind(" src --include="*.luau"
grep -rln "red.useHandlers" src/Server | wc -l
grep -rn 'Instance.new("Remote' src --include="*.luau"
sed -n '1,10p' src/Server/Features/Core/ProfileService.luau
grep -n "SaveInventory\|SaveInStudio" src/Shared/Class/FeatureFlags.luau
head -3 src/Shared/Data/Monetization/Generated.luau
cat .luaurc
git log --diff-filter=R --name-status --oneline -- "*.project.json"
find src -name "*.spec.luau"
Line numbers and counts drift with edits — re-grep the symbol rather than trusting a stale number. If any invariant check fails in a project, update that project's architecture notes AND raise it as a bug via roblox-change-control.