Architecture and security
The security model behind ChloeKernel. Clients send intents and never state, every check fails closed, and where code lives decides who can read it.
Four Studio locations define the model. What sits in ReplicatedStorage replicates to every client and can be read
by an exploiter. What sits in ServerScriptService never leaves the server.
ReplicatedStorage.ChloeKernel replicates: generic primitives, networking client, client kitsReplicatedStorage.Game replicates: Channels.luau, client-visible ConfigServerScriptService.ChloeKernelServer never replicates: sessions, validators, data, commerce, anti-exploit, kitsServerScriptService.Game never replicates: game services, rules, loot odds, kit configs, profile schemaThe rest of the framework follows from that split: the server decides every outcome, the client renders and asks. Project layout maps these locations to folders on disk, and the Workspace overview shows where game code goes.
The principles
Section titled “The principles”- The client is a renderer with opinions. It sends intents (“fire weapon 1 in this direction at this time”), never state or outcomes (“I hit X for 50 damage”). The server validates every intent against its own state and is the only thing that changes it.
- Code placement is attack surface. Validators, rate limits, drop tables, AI logic, anti-cheat thresholds and player
data live in
ServerScriptService. The client holds only what it must run: input, prediction, effects and UI. Everything inReplicatedStorage, includingGame.ChannelsandGame/Configundersrc/Shared, is assumed read. Values clients must not see go in a server module undersrc/Server/Game. - Every check fails closed. A validator that throws rejects the payload. A channel with no validator and no
Open = truerejects every payload, including one the game consumes only through itsIntent.<Name>Bus topic (the NetDriver warns once per channel, andsecurityAudit()lists it). A failed data load kicks the player instead of substituting defaults. A save thatValidaterejects, or that the codec could not read back, is refused. A live record thatValidateor a migration rejects fails the load instead of rolling the player back to an older version. Policy’sallowsanswers false for every feature until the player’s PolicyService rules load, and TextFilter returns nil when filtering fails. - The player’s character stays theirs. The kernel never reassigns a player character’s network ownership and never
anchors character parts, so the client keeps simulating its own character and movement always feels local.
Movement Monitor corrections are plain CFrame writes that leave ownership alone, and
movement abilities go through Prediction: the client moves at once, the server validates,
and a rejection rolls the client back. NPCs are the opposite: NPCKit pins them to server
ownership unless an archetype sets
ClientPhysics = true, and Ragdoll pins the limbs of ragdolled NPCs so nearby clients cannot move them. - Some things are out of scope on purpose. Client code is always readable by exploiters, and obfuscation is not
security, so the defense is never trusting the client. Transactions trades only between
two profiles loaded by one DataDriver on one server (
DifferentDataDriversandProfileInactiverefuse the rest), because a cross-server trade cannot be made safe without a coordinator.
What this means in practice
Section titled “What this means in practice”| You want to | The architectural answer |
|---|---|
| Let players buy an item | The client sends ShopKit’s SH_Buy intent. The kit’s fail-closed hook chain re-derives price and balance from server config, then charges before it grants. Client-side price math is UI decoration. |
| Sell a developer product | Receipts saves the grant before it acknowledges the purchase. A grant that returns false declines it: the profile rolls back and the receipt stays open for the next delivery. |
| Make abilities feel instant | Prediction: the client acts at once, the server reconciles, a rejection rolls back. Authority never moves. |
| Deal with speedhackers | Movement Monitor detects and your hook decides policy. Ban through Consequences (Players:BanAsync with escalation from ban history), which also covers suspected alt accounts, instead of Kick. |
| Validate shots from high-ping players | Rewind checks the shot against target positions at the claimed time. It accepts a claim only as old as the shooter’s round trip plus 0.25 s, capped at HistorySeconds (1 s). The server raycasts; the client never reports a hit. |
| Show text one player wrote to others | TextFilter (broadcast for everyone, forUser for one recipient), or a Settings rule with Filter = "Broadcast". Both fail closed. |
| Sell paid random items, or let players trade paid items | Attach Policy and mark those loot tables Paid = true. LootKit and Transactions.atomic refuse with PolicyRestricted for players whose rules forbid it. |
| Know the surface is closed before shipping | kernel:securityAudit() lists unguarded channels, Open channels with handlers, fail-open gate hooks and defaulted rate limits, and the fuzzer drives hostile payloads through every validator. The Studio spec gate fails on any High finding or crashing validator. See Fuzz and security audit. |
Layered defenses on every intent
Section titled “Layered defenses on every intent”A client-to-server payload passes these checks in order, and each one drops it before any of the later work runs:
- Transport byte window. Packet charges every remote call at least 800 bytes against a per-player window of 8,000
bytes that resets each heartbeat (the
RateLimitBytesattribute on the Packet module). Over the window, the call is dropped before decoding, so garbage cannot buy CPU. Flood reports reach the kernel asNet.FloodDetected, coalesced to one report per player and reason per window. - Shape. A call whose payload is not a buffer, or whose Instance list is not a table, is reported as
Malformedand dropped. - Message budget. Each frame in the payload takes a token from a per-player bucket (240 messages per second, burst
128, set by
RateLimitMessagesandRateLimitMessageBurst) before it decodes. An empty bucket discards the rest of the payload. - Schema decode. Each frame decodes against its channel’s schema. An unknown channel id or a frame that does not end
at its boundary drops the rest of the payload. Typed arrays stop at 8,192 entries per message, an
Instanceslot must hold an Instance, and a duplicate of a request whose handler is still running is skipped. - Session. A live session must exist, and a profile locked by a trade in progress rejects (
TransactionLocked). - Channel rate limit. A token bucket per player per channel (
Net.RateLimited). - Validator presence. A channel with no validator and no
Open = truerejects the payload here. - Declarative rules.
Range,OneOfandMaxLengthfrom the Registry, installed as a priority 5 hook.Rangerejects NaN. - Game validators. The fail-closed hook chain, checking server state only. A validator may yield, so the NetDriver checks again afterwards that the session is still active and the profile is not locked by a trade.
- The handler, which runs only after everything above passed. A handler that throws counts as a rejection, and a predicted intent acks a rollback.
In the published benchmarks, a token bucket take costs 58 ns and a three-validator hook chain 148 ns, so the validation steps cost well under a microsecond per payload. Decode cost depends on the schema. Rejected payloads cost the client nothing and the server only the checks that ran before the rejection.