Guide: Saving Data
Config files that survive a restart, encoding them safely, and the mistakes that corrupt them.
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.
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.
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.
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.
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.
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.
See Filesystem for the full path surface, Crypt for encryption, hashing and randomness, and Encoding for the standalone Base64 and LZ4 entries.