World Executor · Roblox VM

Guide: Saving Data

Config files that survive a restart, encoding them safely, and the mistakes that corrupt them.

The shape of it

The filesystem is the only thing in the executor that outlives the session. Anything you want to keep — a config, a whitelist, a log — goes through writefile and comes back through readfile.

Roblox's own HttpService handles the JSON, so you do not need an encoder.

A config file

config.lua
local HttpService = game:GetService("HttpService")
local PATH = "config/mytool.json"

local DEFAULTS = {
    enabled = true,
    walkSpeed = 32,
    theme = "ocean",
}

local function load()
    if not isfile(PATH) then
        return table.clone(DEFAULTS)
    end

    local ok, decoded = pcall(HttpService.JSONDecode, HttpService, readfile(PATH))
    if not ok or type(decoded) ~= "table" then
        warn("config was corrupt, falling back to defaults")
        return table.clone(DEFAULTS)
    end

    -- Fill in anything a newer version added.
    for key, value in pairs(DEFAULTS) do
        if decoded[key] == nil then
            decoded[key] = value
        end
    end
    return decoded
end

local function save(config)
    if not isfolder("config") then
        makefolder("config")
    end
    writefile(PATH, HttpService:JSONEncode(config))
end

local config = load()
config.walkSpeed = 48
save(config)

Always pcall the decode

A half-written file, a manual edit, or a version bump will hand JSONDecode something it cannot parse. Without the pcall that error surfaces at load time on every single join.

Binary and compression

For anything that is not text — a serialised buffer, an image you fetched — Base64 keeps it safe through a text file, and LZ4 keeps it small. Both are available bare and under the crypt namespace.

blob.lua
local raw = readfile("assets/blob.bin")

local packed = lz4compress(raw)
writefile("cache/blob.lz4", packed)

-- ... and back
local restored = lz4decompress(readfile("cache/blob.lz4"), #raw)

-- Base64 when it has to survive as text.
writefile("cache/blob.b64", base64encode(raw))
local decoded = base64decode(readfile("cache/blob.b64"))

lz4decompress needs the original size, so store it alongside the compressed data — a two-key JSON wrapper is usually the simplest answer.

Anything sensitive

If a file holds something you would rather not leave in plain text on disk — a webhook URL, a key — the crypt library will encrypt it, hash it, or produce the random bytes you need for a key.

crypt.lua
local key = crypt.generatekey()
local encrypted = crypt.encrypt("https://discord.com/api/webhooks/...", key)
writefile("config/hook.enc", encrypted)

-- Hashing, when you only need to compare rather than recover.
print(crypt.hash("compare me"))
print(crypt.hmac("message", key))

The key has to live somewhere

Encrypting a file and storing the key next to it buys you very little. This is worth doing against casual reading, not against someone with your workspace folder.

Related reference

See Filesystem for the full path surface, Crypt for encryption, hashing and randomness, and Encoding for the standalone Base64 and LZ4 entries.