Quick Start

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 CombatNet

Because 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.