Skip to content

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 kits
ReplicatedStorage.Game replicates: Channels.luau, client-visible Config
ServerScriptService.ChloeKernelServer never replicates: sessions, validators, data, commerce, anti-exploit, kits
ServerScriptService.Game never replicates: game services, rules, loot odds, kit configs, profile schema

The 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 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 in ReplicatedStorage, including Game.Channels and Game/Config under src/Shared, is assumed read. Values clients must not see go in a server module under src/Server/Game.
  • Every check fails closed. A validator that throws rejects the payload. A channel with no validator and no Open = true rejects every payload, including one the game consumes only through its Intent.<Name> Bus topic (the NetDriver warns once per channel, and securityAudit() lists it). A failed data load kicks the player instead of substituting defaults. A save that Validate rejects, or that the codec could not read back, is refused. A live record that Validate or a migration rejects fails the load instead of rolling the player back to an older version. Policy’s allows answers 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 (DifferentDataDrivers and ProfileInactive refuse the rest), because a cross-server trade cannot be made safe without a coordinator.
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.

A client-to-server payload passes these checks in order, and each one drops it before any of the later work runs:

  1. 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 RateLimitBytes attribute on the Packet module). Over the window, the call is dropped before decoding, so garbage cannot buy CPU. Flood reports reach the kernel as Net.FloodDetected, coalesced to one report per player and reason per window.
  2. Shape. A call whose payload is not a buffer, or whose Instance list is not a table, is reported as Malformed and dropped.
  3. Message budget. Each frame in the payload takes a token from a per-player bucket (240 messages per second, burst 128, set by RateLimitMessages and RateLimitMessageBurst) before it decodes. An empty bucket discards the rest of the payload.
  4. 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 Instance slot must hold an Instance, and a duplicate of a request whose handler is still running is skipped.
  5. Session. A live session must exist, and a profile locked by a trade in progress rejects (TransactionLocked).
  6. Channel rate limit. A token bucket per player per channel (Net.RateLimited).
  7. Validator presence. A channel with no validator and no Open = true rejects the payload here.
  8. Declarative rules. Range, OneOf and MaxLength from the Registry, installed as a priority 5 hook. Range rejects NaN.
  9. 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.
  10. 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.

ChloeKernel v0.9.1. Access to the framework is private and granted individually by Chloe. Games built with it credit CryptedChloes in their description.