Scripting with Lua
Goop2 includes an embedded Lua runtime that lets you add server-side logic to your peer. Lua scripts can power chat commands, validate data, compute scores, enforce game rules, and more.
Enabling Lua
Add the lua section to your goop.json:
{
"lua": {
"enabled": true,
"script_dir": "site/lua",
"timeout_seconds": 5,
"max_memory_mb": 10
}
}
Script types
Chat commands
Files in site/lua/ (not in functions/) are chat commands. A visitor sends a direct message starting with ! and the matching script runs.
File: site/lua/hello.lua
function handle(args)
return "Hello, " .. (args ~= "" and args or "world") .. "!"
end
A visitor typing !hello Alice receives the response Hello, Alice!.
Data functions
Files in site/lua/functions/ are data functions. They are called from the browser via the goop-data.js library and return structured data.
File: site/lua/functions/score-quiz.lua
function call(request)
local answers = request.params.answers
local score = 0
-- scoring logic here
return { score = score, total = #answers }
end
Called from JavaScript:
const result = await Goop.data.call("score-quiz", { answers: [...] });
Available APIs
goop.peer
Information about the calling peer:
goop.peer.id -- Peer ID (e.g. "12D3Koo...")
goop.peer.label -- Display name (if known)
goop.self
Information about the local peer:
goop.self.id -- Local peer ID
goop.self.label -- Local display name
goop.http
HTTP client (requires http_enabled: true):
local body, err = goop.http.get("https://api.example.com/data")
local body, err = goop.http.post("https://api.example.com/submit", {key = "val"})
Only http:// and https:// URLs are allowed. Requests to private/loopback addresses are blocked.
goop.json
JSON encoding and decoding:
local obj = goop.json.decode('{"name":"Alice"}')
local str = goop.json.encode({name = "Bob"})
goop.kv
Persistent key-value store (per script, requires kv_enabled: true):
goop.kv.set("api_key", "secret123")
local key = goop.kv.get("api_key")
goop.kv.del("api_key")
Limited to 1000 keys and 64 KB total per script.
goop.log
Logging:
goop.log.info("processing request")
goop.log.warn("API key missing")
goop.log.error("connection failed")
goop.db
Database access (data functions only):
local rows = goop.db.query("SELECT * FROM posts WHERE _owner = ?", goop.peer.id)
local count = goop.db.scalar("SELECT COUNT(*) FROM responses")
goop.db.exec("UPDATE games SET turn = ? WHERE _id = ?", "O", game_id)
goop.commands()
Returns a list of all loaded chat commands.
Security
Every Lua invocation runs in a fresh, sandboxed VM:
- No filesystem access --
io,loadfile, anddofileare disabled. - No module loading --
requireandpackageare disabled. - No shell execution --
os.execute,os.remove, etc. are disabled. - Hard timeout -- Default 5 seconds, configurable up to 60.
- Memory limit -- Default 10 MB per VM.
- Rate limiting -- Per-peer (30/min) and global (120/min) limits prevent abuse.
Hot reload
Scripts are automatically reloaded when their files change. There is no need to restart the peer. If a script has a syntax error, the previous working version stays active and the error is logged.
Example: weather command
function handle(args)
if args == "" then return "Usage: !weather <city>" end
local key = goop.kv.get("api_key")
if not key then return "Weather API key not configured." end
local url = "https://api.openweathermap.org/data/2.5/weather"
.. "?q=" .. args .. "&appid=" .. key .. "&units=metric"
local body, err = goop.http.get(url)
if err then return "Error: " .. err end
local data = goop.json.decode(body)
return string.format("%s: %s C, %s",
data.name,
tostring(math.floor(data.main.temp)),
data.weather[1].description)
end
Example: game move validation
function call(request)
local game_id = request.params.game_id
local position = tonumber(request.params.position)
local rows = goop.db.query("SELECT * FROM games WHERE _id = ?", game_id)
if not rows or #rows == 0 then
error("game not found")
end
local game = rows[1]
if game.turn ~= goop.peer.id then
return { error = "not your turn" }
end
local idx = position + 1
if string.sub(game.board, idx, idx) ~= "-" then
return { error = "cell occupied" }
end
local new_board = string.sub(game.board, 1, idx - 1)
.. "X"
.. string.sub(game.board, idx + 1)
goop.db.exec("UPDATE games SET board = ?, turn = ? WHERE _id = ?",
new_board, "O", game_id)
return { board = new_board, turn = "O" }
end