Skip to main content
Flux.Server and Flux.Client are context-guarded. Each one throws if called from the wrong side:
Flux has no unified constructor. There is no Flux.ReferenceBridge, Flux.Event or auto-detecting factory in the public API.The Bridge module inside Flux is unrelated. It is the internal buffer batching and flush layer, not an environment selector, and it is not re-exported from the root module. See Bridge if you are curious what it actually does.
So a ModuleScript required from both sides cannot call either constructor unconditionally. Branch on RunService instead.

The pattern

ReplicatedStorage/Net.luau
Now both sides require the same module and get the object appropriate to their context:
This also happens to be the right place to keep schemas, since both sides have to declare identical layouts. One definition, so the two cannot drift. Compare with the shared-module approach in Schemas.

Keeping types accurate across the branch

The helper above returns a union of two different shapes, which the type checker cannot narrow at the call site. Give each side its own typed view instead:
ReplicatedStorage/Net.luau
Because the argument packs, ChatArgs and DamageArgs, are declared once and reused by both views, the server and client signatures can never disagree about what an event carries. They differ only in whether a Player is prepended, which Flux’s generic types handle for you.

Asymmetric behaviour to remember

A shared module hides the context, so it is easy to forget these differences. They do not disappear.
Code that genuinely runs identically on both sides cannot call Connect with one callback signature. If a shared function needs to handle received messages, keep the listener registration in context-specific scripts and let the shared module own only construction.

Next steps

Generics

Declaring argument packs once and reusing them.

Migrating

Coming from raw remotes or a Warp-style API.