Skip to content

Networking

Nova keeps the classic server-authoritative model:

  • A Script runs on the server and owns the DataModel.
  • A LocalScript runs on a client and receives filtered replication.
  • RemoteEvent and RemoteFunction cross the boundary.
  • The server decides what is true; the client is a view and an input source.

FilteringEnabled is enabled for modern places automatically. Saved values from older places are respected, and new places start filtered.

RemoteEvent

-- ReplicatedStorage/Remotes/Server/RequestPurchase (RemoteEvent)
local remote = game:GetService("ReplicatedStorage")
    :WaitForChild("Remotes")
    :WaitForChild("Server")
    :WaitForChild("RequestPurchase")

-- Server
remote.OnServerEvent:Connect(function(player, productId)
    if typeof(productId) ~= "number" then
        return
    end

    -- Validate: price, ownership, cooldown, distance, state machine, ...
    print(player.Name, "requested", productId)
end)

-- Client
remote:FireServer(12345)

FireClient(player, ...) targets one client; FireAllClients(...) targets everyone. The first argument on the server is always the Player, and it is supplied by the engine; do not include it when firing from the client.

RemoteFunction

RemoteFunction is a synchronous request/response. It yields, so never call it from a context where yielding is unsafe.

-- ReplicatedStorage/Remotes/GetProfile (RemoteFunction)
-- Server
local getProfile = game:GetService("ReplicatedStorage")
    :WaitForChild("Remotes")
    :WaitForChild("GetProfile")

getProfile.OnServerInvoke = function(player)
    return {
        Name = player.Name,
        UserId = player.UserId,
    }
end

-- Client
local profile = getProfile:InvokeServer()
print(profile.Name)

Use RemoteFunction for small reads. For anything that changes state, prefer RemoteEvent so the server can validate and replicate asynchronously.

Rules that prevent most bugs

  1. Never trust the client. Validate types, ranges, ownership, cooldowns, and distance on the server.
  2. Rate-limit. A client can fire a remote as fast as its frame rate. Keep a per-player cooldown for expensive handlers.
  3. Wait for replication. A client may not have the target instance yet. Use WaitForChild on both sides.
  4. Do not send Instances you do not need. Send IDs or primitive values, then resolve them on the receiving side.
  5. Handle disconnects. A player can leave between the request and the response; check player.Parent before using it.
  6. Keep the server authoritative for movement and currency. Do not let a client report "I bought this" or "I am at this position" without validation.

Player lifecycle

local Players = game:GetService("Players")

Players.PlayerAdded:Connect(function(player)
    print("joined", player.UserId, player.Name)

    player.CharacterAdded:Connect(function(character)
        local humanoid = character:WaitForChild("Humanoid")
        humanoid.Died:Connect(function()
            print(player.Name, "died")
        end)
    end)
end)

Players.PlayerRemoving:Connect(function(player)
    print("left", player.UserId)
end)

Players.CharacterAutoLoads controls automatic character spawning. Player:LoadCharacter() is available if you disable it and spawn manually.

UnreliableRemoteEvent

UnreliableRemoteEvent is not implemented yet. Use RemoteEvent. For high-frequency cosmetic data, batch updates and send at a fixed rate rather than every frame.

What replicates

Property changes on the server replicate to clients. Client-side changes do not replicate back. If a client needs to change shared state, it must ask the server.

Common mistakes:

  • A LocalScript sets part.BrickColor, and other players never see it. The server must set it.
  • A server script changes a GUI that only exists on the client. GUI changes should happen in a LocalScript, driven by a remote.
  • A remote fires before the client has the object. Wait for the object, then fire.

Next