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) |
— |
Prevents ad-hoc remotes and polling |
| 7 |
Session-scoped vs persistent state is an explicit contract: with the standard SaveInventory = false flag, inventory is wiped at profile load AND before save; session currencies live in server memory, never in the profile |
src/Shared/Class/FeatureFlags.luau, wipe blocks in ProfileService |
Cross-session progression rewards must go through persisted currencies/XP only. Case study (shipped firebit game, 2026): runs are roguelike — in-run gear and the session currency reset every session, so a subscription's daily bonus had to pay persistent coins, not session scrap |
| 8 |
If the project owns spawning: Players.CharacterAutoLoads = false in the place project JSON, and one service (PlayerService) spawns via CollectionService-tagged parts |
place *.project.json, spawn-tag constant in src/Shared/Data/ |
Roblox default auto-respawn fights custom spawn/death logic (permadeath, revive-at-location) |
| 9 |
Every gameplay gate is server-side first; the client copy is cosmetic. State that gates interaction is set by the server (often as an Attribute) and re-checked in the server handler |
grep the gate attribute in the owning service |
Client-side gating alone is exploitable. Case study (shipped firebit game, 2026): entering the defended vehicle at night would unanchor it and hand the client network ownership — the server sets a lock attribute AND refuses the enter request while locked |
| 10 |
Monetization IDs resolve by NAME through generated code: rblxsync run writes src/Shared/Data/Monetization/Generated.luau ("Do not edit manually"); the sibling init.luau adds by-name lookups; misses warn loudly and return 0 |
src/Shared/Data/Monetization/{Generated,init}.luau |
rblxsync can reissue IDs; names are the stable key. Check how badges resolve in YOUR project — some projects hand-paste badge IDs into a data module as a sanctioned exception; a 0 ID means "not configured" and the award path must no-op with a warn |
| 11 |
Terminal game-state transitions (game over, round end) are idempotent single fan-outs: a guard bool + one signal every subscriber shuts down from |
the game-state service |
The terminal condition can be reported from multiple paths; double-firing teardown corrupts everything downstream |
| 12 |
Multiple Rojo projects, one codebase: default.project.json (slim dev: src + Packages + DevPackages + version.txt → ReplicatedStorage.Version) plus per-place build projects (e.g. level.project.json, lobby.project.json) that add assets and set a ReplicatedStorage.Environment StringValue; a place utility module reads it and services self-gate on it instead of forking the tree |
repo root; src/Shared/Modules/PlaceUtility.luau |
One codebase ships multiple places |
| 13 |
Global strict typing: .luaurc sets "languageMode": "strict" — never add --!strict directives per file (redundant, per the project's CLAUDE.md; generated files are the exception) |
.luaurc at repo root |
Whole-repo type safety; also why require cycles hard-fail (decision 2) |
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" # pin this project's list
grep -rln "red.useHandlers" src/Server | wc -l # pin this project's count
grep -rn 'Instance.new("Remote' src --include="*.luau" # matches the sanctioned list?
sed -n '1,10p' src/Server/Features/Core/ProfileService.luau # choke-point docstring intact?
grep -n "SaveInventory\|SaveInStudio" src/Shared/Class/FeatureFlags.luau
head -3 src/Shared/Data/Monetization/Generated.luau # machine-owned header intact?
cat .luaurc # strict mode global?
git log --diff-filter=R --name-status --oneline -- "*.project.json" # doc-drift candidates
find src -name "*.spec.luau" # test coverage reality check
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.
1---2name: roblox-architecture-contract3description: 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.4---56# Roblox Architecture Contract (firebit house stack)78## Overview910Every 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.1112All 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.1314## When to use1516- Before adding a service, controller, remote, data field, or replication path.17- When a change touches boot order, networking, profile data, spawning, monetization IDs, or a core game loop.18- When code looks wrong ("misspelled key", "dead engine behind a flag", "dev flag always on") and you are tempted to fix it.1920## When NOT to use2122- Deciding whether an action is allowed at all (push, rblxsync run, deleting legacy code, tuning numbers) → **roblox-change-control** owns the four hard rules.23- Per-flag catalog and how to add a config axis → **roblox-config-and-flags**.24- Bootstrap/toolchain/sourcemap problems → **roblox-build-and-env**.25- Roblox platform theory (why Attributes replicate, what ReplicatedFirst is) → **roblox-platform-reference**.26- Symptom-driven bug triage → **roblox-debugging-playbook**. History of why decisions were fought over → **roblox-failure-archaeology**.2728## Boot narrative (60 seconds)2930Jargon: **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.3132Server: `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:3334- 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.35- 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).3637Each 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.3839## Load-bearing decisions (house stack — verify each in the current repo)4041| # | Decision | Where (house-standard location) | Why |42|---|----------|--------------------------------|-----|43| 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 |44| 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 |45| 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 |46| 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 |47| 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 |48| 6 | Three replication channels, each with a correct use (table below) | — | Prevents ad-hoc remotes and polling |49| 7 | Session-scoped vs persistent state is an explicit contract: with the standard `SaveInventory = false` flag, inventory is wiped at profile load AND before save; session currencies live in server memory, never in the profile | `src/Shared/Class/FeatureFlags.luau`, wipe blocks in `ProfileService` | Cross-session progression rewards must go through persisted currencies/XP only. Case study (shipped firebit game, 2026): runs are roguelike — in-run gear and the session currency reset every session, so a subscription's daily bonus had to pay persistent coins, not session scrap |50| 8 | If the project owns spawning: `Players.CharacterAutoLoads = false` in the place project JSON, and one service (PlayerService) spawns via CollectionService-tagged parts | place `*.project.json`, spawn-tag constant in `src/Shared/Data/` | Roblox default auto-respawn fights custom spawn/death logic (permadeath, revive-at-location) |51| 9 | Every gameplay gate is server-side first; the client copy is cosmetic. State that gates interaction is set by the server (often as an Attribute) and re-checked in the server handler | grep the gate attribute in the owning service | Client-side gating alone is exploitable. Case study (shipped firebit game, 2026): entering the defended vehicle at night would unanchor it and hand the client network ownership — the server sets a lock attribute AND refuses the enter request while locked |52| 10 | Monetization IDs resolve by NAME through generated code: `rblxsync run` writes `src/Shared/Data/Monetization/Generated.luau` ("Do not edit manually"); the sibling `init.luau` adds by-name lookups; misses warn loudly and return 0 | `src/Shared/Data/Monetization/{Generated,init}.luau` | rblxsync can reissue IDs; names are the stable key. Check how badges resolve in YOUR project — some projects hand-paste badge IDs into a data module as a sanctioned exception; a 0 ID means "not configured" and the award path must no-op with a warn |53| 11 | Terminal game-state transitions (game over, round end) are idempotent single fan-outs: a guard bool + one signal every subscriber shuts down from | the game-state service | The terminal condition can be reported from multiple paths; double-firing teardown corrupts everything downstream |54| 12 | Multiple Rojo projects, one codebase: `default.project.json` (slim dev: src + Packages + DevPackages + `version.txt` → `ReplicatedStorage.Version`) plus per-place build projects (e.g. `level.project.json`, `lobby.project.json`) that add assets and set a `ReplicatedStorage.Environment` StringValue; a place utility module reads it and services self-gate on it instead of forking the tree | repo root; `src/Shared/Modules/PlaceUtility.luau` | One codebase ships multiple places |55| 13 | Global strict typing: `.luaurc` sets `"languageMode": "strict"` — never add `--!strict` directives per file (redundant, per the project's CLAUDE.md; generated files are the exception) | `.luaurc` at repo root | Whole-repo type safety; also why require cycles hard-fail (decision 2) |5657All 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`.5859### The three replication channels — pick correctly6061| Channel | Direction | Use for | Example |62|---|---|---|---|63| 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 |64| Replica (profile replica, class `"Profile"`) | server→client, state-shaped | Persistent profile data the UI renders | the client data controller subscribes; UI reads client state |65| 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 |6667Do 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.6869## Invariants — each with its proof command7071Run 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).7273| Invariant | One-line verification |74|---|---|75| `red.bind` sites are few and known — pin the list | `grep -rn "red.bind(" src --include="*.luau"` |76| Server handlers all mount via `useHandlers` | `grep -rln "red.useHandlers" src/Server` |77| No unsanctioned raw remotes in src | `grep -rn 'Instance.new("Remote' src --include="*.luau"` → matches your pinned list |78| ProfileService is still the declared write choke point | `sed -n '1,10p' src/Server/Features/Core/ProfileService.luau` |79| 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` |80| Session currency never touches the profile | grep the currency's storage in its service — in-memory table, no profile writes |81| CharacterAutoLoads=false in shipping place projects (if project owns spawning) | `grep -n "CharacterAutoLoads" *.project.json` |82| Spawning only via tagged parts | grep the spawn-tag constant across `src/Server` and `src/Shared/Data` |83| Server re-checks gameplay gates in handlers | grep the gate attribute name in the owning service and its handlers |84| 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 |85| Generated.luau is machine-owned | `head -3 src/Shared/Data/Monetization/Generated.luau` |86| Terminal-state fan-out is idempotent | read the game-state service's end-game function: guard bool + single signal |87| Strict mode global | `cat .luaurc` |8889## Auditing a project's weak points9091The 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**).92931. **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**.942. **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.953. **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.964. **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**.975. **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.986. **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.99100## Common mistakes101102- Naming a plain helper module `*Service`/`*Controller` — Bootstrap will require and boot it.103- 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).104- Mutating profile data directly and forgetting `ProfileService.update()` — the server is right, the client is stale, and nothing errors.105- 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.106- 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).107- Gating purchases, entry, or revives on the client only — every gate is server-side first (decisions 5, 9); the client copy is cosmetic.108- Using `Instance.new` for UI or adding a raw RemoteEvent — React-only UI (see **luau-react**) and red-only networking.109- "Cleaning up" misspelled persisted keys or dormant flag-gated subsystems.110- Trusting a derived dev/production flag without reading how its place-ID constant is set.111112## Provenance and maintenance113114Derived 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.115116Re-verification commands run against the CURRENT project repo, from its root:117118```sh119grep -rn "red.bind(" src --include="*.luau" # pin this project's list120grep -rln "red.useHandlers" src/Server | wc -l # pin this project's count121grep -rn 'Instance.new("Remote' src --include="*.luau" # matches the sanctioned list?122sed -n '1,10p' src/Server/Features/Core/ProfileService.luau # choke-point docstring intact?123grep -n "SaveInventory\|SaveInStudio" src/Shared/Class/FeatureFlags.luau124head -3 src/Shared/Data/Monetization/Generated.luau # machine-owned header intact?125cat .luaurc # strict mode global?126git log --diff-filter=R --name-status --oneline -- "*.project.json" # doc-drift candidates127find src -name "*.spec.luau" # test coverage reality check128```129130Line 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**.