Skip to content

BusBridge

Carries Bus topics across the client and server boundary: server topics broadcast to every client, and client topics from an exact-name whitelist come up as rate-limited intents.

Realm
Server + Client
Require
ServerScriptService.ChloeKernelServer.BusBridge
Since
v0.3.0
Source
BusBridge.luauNet/BusBridgeClient.luau

The Bus has a NetBridge slot, and BusBridge fills it. Attach it once on each side and marked topics cross the wire as ordinary Bus publishes, with no channel code per event. The two directions are deliberately different. Downward (server to client) is a broadcast of whatever the server chooses to say. Upward (client to server) is an intent like any other. Only whitelisted topic names get a channel, each payload decodes against a schema, each topic has a per-player rate limit, and the fail-closed hook chain runs before the Bus sees anything. Nothing a client publishes reaches the server Bus except through the full NetDriver intent pipeline.

Two independent paths share one attach call.

Every forwarded server topic rides one state channel, BusBridge (CKS_BusBridge on the wire), with schema String, Any: the topic name and the packed argument table. Two things forward a topic:

  • Kernel.Bus:publishRemote(topic, ...): explicit, any topic, per call site.
  • A topic matching ServerTopics: existing publish call sites cross with no code change. Trailing wildcards ("Round.*") work here, as they do in Bus subscriptions.

The arguments are packed with table.pack, so the count travels with them and a nil argument no longer drops the arguments after it on the client (fixed in 0.9.0). Each client republishes the topic on its own Bus, so client code subscribes with kernel.Bus:subscribe and cannot tell a bridged topic from a local one.

Each entry in ClientTopics defines its own intent channel, BusPublish.<Topic> (CKI_BusPublish.<Topic> on the wire), with the schema from ClientSchemas and the bridge’s rate limit. A client publishRemote fires that intent. The server republishes the topic on its Bus with the sender’s session prepended. The session comes from the server’s own session table, so nothing in the payload can name a different sender.

A client publish delivered on the server Bus could match a ServerTopics pattern and forward straight back down, sending the session object and attacker-chosen arguments to every client. While an upward publish is being delivered, the bridge suppresses the downward forward of that same topic. Only the reflected topic is blocked: a server topic that a subscriber publishes synchronously in response (a reply on another topic) still goes down. Before 0.9.0 the guard blocked every downward forward during delivery, so those replies were lost. The guard saves and restores the previous topic around the publish, so nested deliveries unwind correctly, and it resets even when the publish throws.

local ServerScriptService = game:GetService("ServerScriptService")
local BusBridge = require(ServerScriptService.ChloeKernelServer.BusBridge)
BusBridge.attach(Kernel, {
ServerTopics = { "Round.*", "Combat.Kill" }, -- forward to every client
ClientTopics = { "Emote.Play" }, -- exact names clients may publish upward
ClientSchemas = { ["Emote.Play"] = { "NumberU8" } },
RateLimit = 20, -- per player, per upward topic, per second
})
-- Any topic, explicitly, without listing it in ServerTopics:
Kernel.Bus:publishRemote("Server.Announcement", "Double XP weekend")
-- Client publishes arrive validated, with the session first:
Kernel.Bus:subscribe("Emote.Play", function(_, session, emoteId: number)
playEmoteFor(session.Player, emoteId)
end)

Downward forwards go to every client through broadcastState. The bridge has no per-topic interest scoping. Client subscribers receive (topic, ...args) exactly as a local publish would deliver them.

Order is preserved. All downward topics share one reliable channel, so clients see bridged publishes in the server’s publish order, across topics. A client that attaches late loses nothing recent: parked frames for CKS_BusBridge replay once BusBridgeClient.attach constructs the channel, up to 64 frames and 10 seconds.

An upward publish passes every gate an ordinary intent passes, in order:

  1. Exact-name whitelist, fail-closed. Only topics listed in ClientTopics have a channel at all. A client firing Admin.GrantCoins is not rejected; there is nothing to fire at, and nothing reaches the Bus. With no ClientTopics, nothing goes up.
  2. Channel-name rules. BusPublish.<Topic> must pass the Registry name check: letters, digits and . / - _, at most 94 characters in total (so a topic of at most 83), never starting with RBX. attach errors on a topic that breaks them, for example BusBridge client topic "Combat.*" (channel "BusPublish.Combat.*"): names use only letters, digits and . / - _. An upward wildcard is therefore an attach error. Before 0.9.0 it defined a channel no published topic could match.
  3. Typed schema. Every upward topic needs a ClientSchemas entry, or attach errors with BusBridge client topic "<Topic>" needs a ClientSchemas entry. Payloads decode through the declared wire types.
  4. Session gate and rate limit. A sender with no active session, or whose profile is locked by a transaction, is dropped. Each topic then has a per-player token bucket at RateLimit per second (default 20). Flooding a topic throttles that player on that topic and publishes Net.RateLimited.
  5. The hook chain. Intent.BusPublish.<Topic> fires before the publish, fail-closed. A validator returning false or crashing rejects the message. Game rules go here.
  6. The Bus. Only then does kernel.Bus:publish(topic, session, ...) run on the server.

A validator reads the decoded arguments and the server’s own session:

Kernel.Hooks:on("Intent.BusPublish.Emote.Play", function(context)
local Owned = OwnedEmotes[context.Session.Player.UserId]
if Owned == nil or not Owned[context.Args[1]] then
return false -- not owned: rejected, never reaches the Bus
end
return nil
end)
Member Description
BusBridge.attach(kernel, options?) Defines the downward state channel, installs the Bus NetBridge, subscribes the ServerTopics forwards and defines one intent per ClientTopics entry. Call once, in the server bootstrap

attach options:

Field Type Description
ServerTopics { string }? Topic patterns that forward on an ordinary publish. Trailing wildcards allowed
ClientTopics { string }? Exact topic names clients may publish upward. Each BusPublish.<Topic> must pass the channel-name rules. Absent means nothing goes up
ClientSchemas { [string]: { any } }? Topic to wire-type array. Required for every ClientTopics entry
RateLimit number?, default 20 Per-player, per-topic upward rate, messages per second
Member Description
BusBridgeClient.attach(kernel, netClient?, options?) Listens on the downward channel, builds an intent handle per ClientSchemas topic and installs the client Bus NetBridge. netClient defaults to a new NetClient; passing the options table in its place also works

attach options:

Field Type Description
ClientSchemas { [string]: { any } }? Topic to wire-type array for upward publishes. Must match the server’s ClientSchemas
Member Description
Bus:publishRemote(topic, ...) Publishes locally, then hands the topic to the bridge. Server: broadcast down. Client: fire the upward intent if the topic has a schema
Bus:setNetBridge(bridge) The slot attach fills. Game code does not call it
Point Kind Description
Intent.BusPublish.<Topic> hook chain, fail-closed Game validators for an upward topic. Context carries Name, Player, Session, Args
<Topic> (server Bus) Bus topic The bridged publish, delivered as (topic, session, ...args)
Intent.BusPublish.<Topic> (server Bus) Bus topic NetDriver’s publish after the underlying intent was accepted, (topic, session, ...args)
Net.IntentRejected Bus topic (player, channelName): an upward publish failed validation
Net.RateLimited Bus topic and fail-open hook (player, channelName): an upward publish exceeded the topic’s rate
<Topic> (client Bus) Bus topic Downward forwards, republished locally as (topic, ...args)

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