Rivo FX

Rivo FX gives Lua and JS/TS resources the model Effect made popular: errors as values, fibers that clean up after themselves, services through Layers, and Schema at every trust boundary. On top of that it adds the FiveM pieces scripts get wrong most often:

  • Net events that validate first. A handler never sees a payload that fails its schema, and every player is rate limited per event by default.
  • RPC with timeouts and typed errors. A NotEnoughMoney raised on the server arrives on the client as NotEnoughMoney, so catchTag works across the wire. Defects never leak details to clients.
  • Cleanup that actually runs. Scopes close when the resource stops or the player drops. Spawned entities get deleted, models released and in-flight handlers interrupted.
  • No more while not HasModelLoaded(m) do Wait(0) end. Asset loads have timeouts and typed errors.

It also comes with metrics and tracing and in-game devtools (/fxdev). Free and MIT licensed. Lua 5.4, FiveM. Source on GitHub.

Install

  1. Download rivo_fx.zip from the latest release, unzip it into your resources folder (the folder must be named rivo_fx) and ensure rivo_fx before anything that uses it.
  2. In your resource:
fx_version 'cerulean'
game 'gta5'
lua54 'yes'

dependency 'rivo_fx'
shared_script '@rivo_fx/init.lua'

The library loads into your resource's own runtime, the way ox_lib does. No exports sit on the hot path, and FiveM modules load on first use.

Types: add rivo_fx/types to your LuaLS library:

{ "workspace.library": ["../rivo_fx/types"] }

JS / TS resources: npm install @rivolabs/fx effect (see JavaScript, TypeScript and NUI).

A taste

local S = Fx.S
local NotEnoughMoney = Fx.TaggedError('NotEnoughMoney')

-- a service, provided by a Layer
local Bank = Fx.Tag('Bank')
local BankLive = Fx.Layer.succeed(Bank, {
  withdraw = function(src, amount)
    return Fx.suspend(function()
      local have = balances[src] or 0
      if have < amount then return NotEnoughMoney { have = have, need = amount }:fail() end
      balances[src] = have - amount
      return Fx.succeed(balances[src])
    end)
  end,
})
Fx.setRuntime(Fx.Runtime.make(BankLive))

-- server: validated, rate limited, typed
Fx.rpc.register('bank:withdraw', { input = S.struct { amount = S.int:between(1, 1e6) } },
  function(src, input)
    return Fx.gen(function()
      local bank = Fx.yield(Bank)
      return Fx.yield(bank.withdraw(src, input.amount))
    end)
  end)
-- client
Fx.runFork(Fx.gen(function()
  local balance = Fx.yield(
    Fx.rpc.call('bank:withdraw', { amount = 500 }, { timeout = 5000 })
      :catchTag('NotEnoughMoney', function(e)
        return Fx.succeed(e.have)
      end)
  )
  print('balance now', balance)
end))

Start a resource

npx rivo-fx create my_shop                      # server + client
npx rivo-fx create my_shop --template lua-nui   # plus a page using @rivolabs/fx-nui

Moving an existing resource over? The migration guide has before/after code for net events, ox_lib/ESX/QBCore callbacks, framework money, oxmysql, asset loading, threads and NUI.

Level 0: use it without learning Effect

Already have a codebase? Start with Fx.runAwait. It runs an effect from any Citizen thread and returns the value, or raises the error so pcall works:

CreateThread(function()
  local hash = Fx.runAwait(Fx.assets.model('adder', { timeout = 5000 }))
  -- ... spawn something
end)

Read next

What's next

  • OpenTelemetry (OTLP) export of spans and metrics
  • A layer graph view in /fxdev
  • An ox_core framework layer
  • A TypeScript resource template for rivo-fx create

Found a bug or want to help? See CONTRIBUTING. Security issues go through private reporting.