Plugins
Plugins are optional hook tables that observe or extend Rivet runtime behavior. They are useful for logging, diagnostics, metrics, tracing, policy checks, and other cross-cutting concerns that should not be copied into every Unit.
local LogPlugin = {}
LogPlugin.Id = "LogPlugin"
function LogPlugin:OnUnitStart(unit)
print("Started Unit", unit.Id)
end
return LogPlugin
Register plugins before startup:
local LogPlugin = require(script.LogPlugin)
Rivet.Use(LogPlugin)
Rivet.Start({
Roots = {
ReplicatedStorage.Units,
},
})
Rivet.StartOnce startup begins, Rivet locks plugin registration. Late plugin registration throws because plugins need deterministic lifecycle order.
Plugin Shape
A plugin is a table with an Id and optional hook functions.
local Plugin = {
Id = "MyPlugin",
}
function Plugin:Init(rivet)
end
function Plugin:Start(rivet)
end
function Plugin:OnDestroy()
end
return Plugin
Id must be a non-empty string and must be unique across registered plugins. Hook fields must be functions when present.
Hook List
Rivet gives plugins hooks around the runtime lifecycle, Unit lifecycle, surfaces, network calls, and shutdown.
Use Init(rivet) when the plugin needs to initialize after networking setup but before Unit Init methods run. Use Start(rivet) when the plugin needs to wait until every Unit has finished Start.
For Unit-level work, OnUnitLoaded(unit) fires after a ModuleScript is required and normalized, OnUnitPrepared(unit) fires after runtime helpers are attached, OnUnitInit(unit) fires after the Unit's Init, and OnUnitStart(unit) fires after the Unit's Start.
For public API and networking work, OnSurfaceRegistered(unit, surface) fires for each Client surface after networking setup. OnNetworkCall(context) fires when a network surface is called or a Signal is fired. OnNetworkError(context) fires when that network path fails.
During shutdown, OnDestroy() runs after Rivet has destroyed the Unit runtime.
Startup Hook Order
For a simple Data -> Inventory graph, hook order looks like this:
OnUnitLoaded(Data)
OnUnitLoaded(Inventory)
OnUnitPrepared(Data)
OnUnitPrepared(Inventory)
OnSurfaceRegistered(...)
Init(rivet)
Data:Init()
OnUnitInit(Data)
Inventory:Init()
OnUnitInit(Inventory)
Data:Start()
OnUnitStart(Data)
Inventory:Start()
OnUnitStart(Inventory)
Start(rivet)
Hook order matters when plugins collect metadata. If you need to see normalized Units before lifecycle methods run, use OnUnitPrepared. If you need to see Units after their own Init, use OnUnitInit. If you need all Unit Start methods to finish first, use plugin Start.
Example: Startup Logger
--!strict
local StartupLogger = {}
StartupLogger.Id = "StartupLogger"
function StartupLogger:OnUnitLoaded(unit)
print("[Rivet] loaded", unit.Id)
end
function StartupLogger:OnUnitInit(unit)
print("[Rivet] initialized", unit.Id)
end
function StartupLogger:OnUnitStart(unit)
print("[Rivet] started", unit.Id)
end
return StartupLogger
Register:
Rivet.Use(require(ReplicatedStorage.Plugins.StartupLogger))
Example: Network Metrics
Network hooks receive small context tables.
OnNetworkCall context:
{
UnitId = "Inventory",
SurfaceName = "GetItems",
Kind = "Query",
ArgCount = 1,
}
OnNetworkError context:
{
UnitId = "Inventory",
SurfaceName = "EquipItem",
Kind = "Action",
Message = "Rivet contract failed: ...",
}
Metrics plugin:
local NetworkMetrics = {
Id = "NetworkMetrics",
}
function NetworkMetrics:Init()
self.Calls = {}
self.Failures = {}
end
function NetworkMetrics:OnNetworkCall(context)
local key = `{context.UnitId}.{context.SurfaceName}`
self.Calls[key] = (self.Calls[key] or 0) + 1
end
function NetworkMetrics:OnNetworkError(context)
local key = `{context.UnitId}.{context.SurfaceName}`
self.Failures[key] = (self.Failures[key] or 0) + 1
warn("[Rivet network error]", key, context.Message)
end
return NetworkMetrics
Rivet.Debug:GetNetworkStats() only returns counters when Debug.Network = true, but plugin network hooks are fired from the same call/failure paths.
Example: Surface Policy
Plugins can inspect surface records and enforce project conventions.
local SurfacePolicy = {
Id = "SurfacePolicy",
}
function SurfacePolicy:OnSurfaceRegistered(unit, surface)
if surface.Kind == "Action" and surface.Args == nil then
warn(`Action {unit.Id}.{surface.Name} has no Args contract`)
end
end
return SurfacePolicy
This plugin does not modify Units. It watches the public shape and warns when a convention is missed.
Keep Plugins Focused
Plugins work best when they handle cross-cutting behavior: lifecycle logs, network metrics, startup timings, surface conventions, analytics bridges, and project-specific diagnostics.
They get risky when they hide core gameplay behavior, mutate Units in surprising ways, create undeclared dependencies, replace surface methods, or make startup order harder to understand.
If behavior belongs to one gameplay system, put it in that Unit. If behavior observes or augments the runtime as a whole, use a plugin.
Failure Handling
If a plugin hook errors, Rivet wraps the message with the plugin id and hook name:
Rivet plugin "FailingPlugin" OnUnitLoaded failed: hook exploded
During startup, a plugin failure fails startup. During destroy, OnDestroy failures are reported after Unit runtime destroy runs.
Registering Multiple Plugins
Register plugins in the order you want their hooks to run.
Rivet.Use(require(Plugins.StartupLogger))
Rivet.Use(require(Plugins.NetworkMetrics))
Rivet.Use(require(Plugins.SurfacePolicy))
Rivet calls each hook for every registered plugin in registration order.
Next: Debugging And Errors.