Skip to content

Client and Server

Every synchronized resource can have a client VM, a server VM, or both. Treat the network boundary as a trust boundary.

Choose the owning side

WorkSide
Input, local UI, visual previewsClient
Inventory or container mutationServer
Permissions and rewardsServer
Persistent stateServer
Local player notificationClient or server targeting a client

Network event flow

lua
-- client.lua
RegisterCommand('claim_reward', function()
  TriggerServerEvent('rewards:claim', 'daily')
end, false)
lua
-- server.lua
RegisterNetEvent('rewards:claim', function(rewardId)
  local peerId = source

  if rewardId ~= 'daily' then
    return
  end

  if not Player.HasPermission(peerId, 'rewards.claim') then
    return
  end

  local revision = Inventory.GetRevision(peerId)
  Inventory.AddItem(peerId, 'Coins', 10, 1, revision)
end)

The server handler validates the requested reward, checks permission, and performs the mutation. Never accept an item prefab, quantity, permission result, position, or player identity from the client without server-side validation.

The source value

Inside a network event handler, global source is the sending peer ID. Capture it before starting asynchronous work:

lua
RegisterNetEvent('profile:load', function()
  local peerId = source

  MySQL.single('SELECT title FROM profiles WHERE steam_id = @id', {
    ['@id'] = Player.GetSteamId(peerId)
  }, function(row)
    if row then
      TriggerClientEvent('profile:title', peerId, row.title)
    end
  end)
end)

Local events and exports

Use TriggerEvent for decoupled work on the same side. Use exports for a synchronous resource-to-resource contract:

lua
-- provider/server.lua
exports('getMultiplier', function()
  return 1.25
end)

-- consumer/server.lua
local multiplier = exports['provider'].getMultiplier() or 1

Exports cannot cross the client/server boundary.

Lua resources for Valheim, built on a server-authoritative runtime.