Quick Start
This guide walks you through creating your first Namespace, defining Channels, building the namespace, and sending data between server and client.
1. Installation
Place the KravasNet module as a sibling of ByteNet (it requires it via script.Parent.bytenet), inside a location shared by both server and client — typically ReplicatedStorage.Packages.
local KravasNet = require(ReplicatedStorage.Packages.KravasNet)2. Creating a Namespace
A Namespace is the main organizational unit. You can build it up in three equivalent ways.
local CombatNet = KravasNet.createNamespace("Combat")
CombatNet:add("Damage", {
amount = KravasNet.float32,
target = KravasNet.inst,
})
CombatNet:add("Position", { pos = KravasNet.vec3 }, {
reliability = "unreliable",
})Type constructors like KravasNet.float32, KravasNet.int8, KravasNet.vec3
are used without () — they're pre-built values, not factories.
KravasNet.packet(), KravasNet.optional(), KravasNet.array(),
KravasNet.struct(), and KravasNet.map() are functions and must be called.
3. Building the namespace
This is the step most likely to bite you. ByteNet requires every
packet in a namespace to be registered in a single call, and the
underlying network object (a replicated StringValue) only becomes
readable to the client after the server has built it. If server and
client build the same namespace at very different times, the client can
end up with mismatched packet IDs and silently drop or misroute traffic.
Always call :build() explicitly, right after declaring all of a namespace's channels — in the same ModuleScript, before return:
-- ReplicatedStorage/Networking/Combat.lua
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local KravasNet = require(ReplicatedStorage.Packages.KravasNet)
local CombatNet = KravasNet.createNamespace("Combat")
:add("Damage", { amount = KravasNet.float32 })
:add("Position", { pos = KravasNet.vec3 }, { reliability = "unreliable" })
CombatNet:build() -- registers with ByteNet as soon as this module is required
return CombatNetBecause require() on a shared ModuleScript runs once per side (once on the server's VM, once on each client's VM) as soon as anything first requires it, calling :build() at the bottom guarantees both sides register the namespace at require-time rather than at some unpredictable later point in your game logic. See Best Practices for the full reasoning.
4. Sending data (Server → Client)
-- Server script
local CombatNet = require(ReplicatedStorage.Networking.Combat)
CombatNet.Damage:sendTo(player, { amount = 50, target = player.Character })
CombatNet.Damage:sendToAll({ amount = 10, target = nil })
CombatNet.Damage:sendToAllExcept(player, { amount = 10, target = nil })5. Listening for data (Client)
-- Client script (LocalScript)
local CombatNet = require(ReplicatedStorage.Networking.Combat)
CombatNet.Damage:on(function(data)
print("Took damage:", data.amount)
end)6. Sending data (Client → Server)
-- Client script
CombatNet.Position:send({ pos = character:GetPivot().Position })7. Listening on the server
-- Server script
CombatNet.Position:on(function(data, player)
print(player.Name, "moved to", data.pos)
end)The Channel API is context-aware: KravasNet detects
RunService:IsServer() once, at require-time, and exposes the matching
method set. Calling a server-only method (like sendTo) from the client
— or vice versa — errors immediately, by design, so mistakes fail loudly
instead of silently doing nothing.
8. Standalone channels
If you don't need grouping, skip the namespace entirely:
local HealthChannel = KravasNet.createChannel({
current = KravasNet.uint8,
max = KravasNet.uint8,
})Standalone channels build immediately — there's no separate :build() step for them, since each one is already its own anonymous namespace under the hood.
Next: read the full API Reference for every method and option available, or go straight to Best Practices.