Codecs
Roblox remotes can move primitive values, Instances, arrays, and dictionaries. Game code often works with richer domain values: item objects, loadout entries, profile snapshots, quest records, shop products, and other tables that mean more than their raw shape.
A Rivet codec is an explicit encoder/decoder pair for one named custom type. The encoder turns a domain value into remote-safe data. The decoder rebuilds the domain value on the other side.
Rivet.Codec:Register("Item", {
Encode = function(item)
return {
Id = item.Id,
Count = item.Count,
}
end,
Decode = function(data)
return Item.new(data.Id, data.Count)
end,
})
After registering the codec, use the codec id as a contract name.
Inventory.Surfaces = {
Client = {
GetItem = {
Kind = "Query",
Returns = "Item",
},
UseItem = {
Kind = "Action",
Args = { "Item" },
},
ItemAdded = {
Kind = "Signal",
Payload = { "Item" },
},
},
}
Rivet does not guess from table shape, metatables, or field names. If a contract name is not one of Rivet's built-in primitive names, Rivet expects a codec with that exact id.
When To Use A Codec
Use a codec when the value has domain meaning and should be rebuilt intentionally:
- an
Itemobject with constructor logic - a loadout slot with item and upgrade state
- a profile summary with only safe public fields
- a quest progress record
- a shop product snapshot
- a compact DTO for a large server object
Do not use a codec just because a value is a table. If the value is already a simple remote-safe table and no rebuild step is needed, a table contract may be enough.
Registering A Codec
Register codecs before Rivet.Start.
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Rivet = require(ReplicatedStorage.Packages.Rivet)
local Item = require(ReplicatedStorage.Domain.Item)
Rivet.Codec:Register("Item", {
Encode = function(value: unknown): unknown
local item = value :: Item.Item
return {
Id = item.Id,
Count = item.Count,
}
end,
Decode = function(data: unknown): unknown
local encoded = data :: {
Id: string,
Count: number,
}
return Item.new(encoded.Id, encoded.Count)
end,
})
Rivet.Start({
Roots = {
ReplicatedStorage.Units,
},
})
Codec ids must be non-empty strings and unique. A codec must be a table with function fields named Encode and Decode.
Encoded Data Must Be Remote-Safe
Encode should return values that Roblox remotes can move:
- strings
- numbers
- booleans
- nil when intentionally part of a shape
- Instances
- arrays and dictionaries made from remote-safe values
- buffers if your Roblox target supports them
Avoid returning:
- functions
- threads
- userdata
- cyclic tables
- live connections
- objects that only make sense in one VM
Rivet checks encoded output and fails clearly if it finds unsupported values or cycles.
Rivet codec encode failed at Inventory.GetItem return (server): unsupported function value
A codec should send enough information to rebuild a value. It should not attempt to send methods, callbacks, connections, or private server objects.
Query Return Codec
Server Unit:
Inventory.Surfaces = {
Client = {
GetItem = {
Kind = "Query",
Returns = "Item",
},
},
}
function Inventory:GetItem(player: Player)
return Item.new("Sword", 1)
end
Flow:
- Server method returns an
Item. - Rivet validates the return contract as custom.
- Rivet calls
Item.Encode. - Rivet wraps the encoded table with codec metadata.
- Client proxy receives the encoded value.
- Client proxy calls
Item.Decode. - Client code receives the rebuilt
Item.
Client code:
local item = Inventory:GetItem()
print(item.Id, item.Count)
Action Argument Codec
Client code:
Inventory:UseItem(Item.new("Potion", 3))
Surface:
UseItem = {
Kind = "Action",
Args = { "Item" },
}
Server method:
function Inventory:UseItem(player: Player, item: Item.Item)
print(player.Name, "used", item.Id)
end
Flow:
- Client proxy sees
Args = { "Item" }. - Client calls
Item.Encode. - Server dispatcher receives encoded data.
- Server calls
Item.Decode. - Server validates tuple count and dispatches the method.
Signal Payload Codec
Surface:
ItemAdded = {
Kind = "Signal",
Payload = { "Item" },
}
Server fire:
self.Client.ItemAdded:Fire(player, Item.new("Axe", 2))
Client listen:
Inventory.ItemAdded:Connect(function(item)
print(item.Id, item.Count)
end)
The server encodes the item before firing. The client proxy decodes before calling the callback.
Register On Every Side That Needs It
A side needs a codec when it has to encode or decode a custom value. In practice, register shared codecs in code both the server and client can require.
For example:
ReplicatedStorage
Domain
Item
Codecs
RegisterRivetCodecs
--!strict
local Item = require(script.Parent.Parent.Domain.Item)
return function(Rivet)
Rivet.Codec:Register("Item", {
Encode = function(value)
return {
Id = value.Id,
Count = value.Count,
}
end,
Decode = function(data)
return Item.new(data.Id, data.Count)
end,
})
end
Server and client boot can both call the registration function before Rivet.Start.
local registerCodecs = require(ReplicatedStorage.Codecs.RegisterRivetCodecs)
registerCodecs(Rivet)
If the same runtime calls Rivet.Codec:Register("Item", ...) twice, Rivet errors with a duplicate codec id. Put registration in a predictable boot path and avoid requiring the registration module multiple times in the same runtime.
Codec Design Advice
Prefer small encoded shapes. A codec should send the data the receiver needs, not a full private server object.
Use explicit field names. { Id = item.Id, Count = item.Count } is easier to debug than positional arrays unless size pressure truly matters.
Version complex shapes when they may change.
return {
Version = 1,
Id = item.Id,
Count = item.Count,
Tags = table.clone(item.Tags),
}
Validate in Decode if the data comes from the client. Codecs transform values, but server game rules still need to reject impossible or unauthorized data.
Common Failures
Missing codec:
Rivet missing codec. Unit "Inventory" surface "GetItem" uses unsupported type "Item"; register a codec with Rivet.Codec:Register("Item", codec).
Duplicate codec:
Rivet duplicate codec id "Item"
Encode failure:
Rivet codec encode failed at Inventory.GetItem return (server): ...
Decode failure:
Rivet codec decode failed at Inventory.UseItem arg #1 (server): ...
These messages include the Unit, surface, contract position, and side so you know which codec and remote boundary to inspect.
Next: Plugins.