Storage
Use storage APIs to persist data safely.
Storage: server-authoritative data.LocalStorage: client-local cache/preferences.- default built-in server backend: SQLite at
runtime/mtadm.db. - account/auth data and
Storagedata share the same SQLite database file. - built-in moderation bans are persisted in this database via the dedicated
banstable.
Prerequisite:
- Storage runtime module must be enabled for
Storage.*/LocalStorage.*availability.
Error Handling Pattern​
This API uses the "Error or Nil" pattern.
- If a function succeeds,
errisnil. - If a function fails,
erris a string code/message.
Function List​
| Function | Description | Scope |
|---|---|---|
Storage.Get | Read one server-authoritative record by bucket/key. | S |
Storage.Set | Create or replace one server-authoritative record. | S |
Storage.Delete | Delete one server-authoritative record. | S |
Storage.Update | Atomic read-modify-write on one record. | S |
Storage.Query | Query records with bounded filters and paging. | S |
Storage.Increment | Atomic numeric increment/decrement. | S |
LocalStorage.Get | Read one local client value. | C |
LocalStorage.Set | Write one local client value. | C |
LocalStorage.Delete | Delete one local client value. | C |
LocalStorage.List | List local client values in a namespace. | C |
Static Service: Storage​
Storage is authoritative and server-only.
| Function | Description | Scope |
|---|---|---|
Storage.Get | Read one record by bucket/key. | S |
Storage.Set | Create or replace one record. | S |
Storage.Delete | Delete one record by bucket/key. | S |
Storage.Update | Atomic read-modify-write with a mutator callback. | S |
Storage.Query | Query records with bounded filter and paging options. | S |
Storage.Increment | Atomic numeric increment/decrement. | S |
Static Service: LocalStorage​
LocalStorage is client-local and non-authoritative.
| Function | Description | Scope |
|---|---|---|
LocalStorage.Get | Read one local value. | C |
LocalStorage.Set | Write one local value. | C |
LocalStorage.Delete | Delete one local value. | C |
LocalStorage.List | List keys/values in a namespace with optional paging. | C |
Event List​
| Event | Description | Scope |
|---|---|---|
Built-in events | None in Storage/LocalStorage v1. | S |
storage:record_updated | Recommended custom event after write/update/increment. | S |
storage:record_deleted | Recommended custom event after delete. | S |
storage:revision_conflict | Recommended custom event for optimistic concurrency conflicts. | S |
Scope Model​
Storage.*is server-only and authoritative.LocalStorage.*is client-only and non-authoritative.- Client-to-server persistence flows should use server APIs (for example
Command/Event) that callStorage.*on server.
Node Relationship​
Storageis persistence for durable data (accounts, bans, economy, progression, config).- Node domains (
NodeSim,NodeSharedVisual,NodePlayers,NodeUI) are runtime state graphs. - Do not treat node runtime state as durable persistence by default.
- If a resource needs persistence for node-backed gameplay data, persist resource-owned records in
Storageand rebuild node state on load/start.
Events​
Storage and LocalStorage do not expose built-in hook callbacks in v1.
Use custom domain events from your storage workflows.
storage:record_updated (Custom)​
Recommended payload:
bucket(string)key(string)revision(number)updatedBy(string): recommendedsystem,player:<playerId>,account:<accountId>, orclient:<clientId>
Example:
TriggerEvent("storage:record_updated", {
bucket = bucket,
key = key,
revision = meta and meta.revision,
updatedBy = meta and meta.updatedBy
})
storage:record_deleted (Custom)​
Recommended payload:
bucket(string)key(string)updatedBy(string): recommendedsystem,player:<playerId>,account:<accountId>, orclient:<clientId>
Example:
TriggerEvent("storage:record_deleted", {
bucket = bucket,
key = key,
updatedBy = "system"
})
storage:revision_conflict (Custom)​
Recommended payload:
bucket(string)key(string)expectedRevision(number)actualRevision(number)
Example:
TriggerEvent("storage:revision_conflict", {
bucket = "economy_balances",
key = key,
expectedRevision = expectedRevision,
actualRevision = currentRevision
})
Storage Functions​
Storage.Get​
S Server Only
local value, meta, err = Storage.Get(bucket, key)
Parameters:
bucket(string): logical bucket/namespace.key(string): record key.
Returns:
value(table | number | string | boolean | nil)meta(table | nil): record metadata.err(nil| string)
Storage.Set​
S Server Only
local meta, err = Storage.Set(bucket, key, value, options)
Parameters:
bucket(string)key(string)value(table | number | string | boolean)options(table, optional): optional fields listed below.expectedRevision(number, optional)ttlSeconds(number, optional)auditReason(string, optional)
Returns:
meta(table | nil)err(nil| string)
Storage.Delete​
S Server Only
local meta, err = Storage.Delete(bucket, key, options)
Parameters:
bucket(string)key(string)options(table, optional): optional fields listed below.expectedRevision(number, optional)auditReason(string, optional)
Returns:
meta(table | nil)err(nil| string)
Storage.Update​
S Server Only
local value, meta, err = Storage.Update(bucket, key, mutatorFn, options)
Parameters:
bucket(string)key(string)mutatorFn(function):nextValue = mutatorFn(currentValue)options(table, optional): optional fields listed below.expectedRevision(number, optional)ttlSeconds(number, optional)auditReason(string, optional)
Returns:
value(table | number | string | boolean | nil)meta(table | nil)err(nil| string)
Storage.Query​
S Server Only
local rows, page, err = Storage.Query(bucket, filter, options)
Parameters:
bucket(string)filter(table): constrained filter object.options(table, optional): optional fields listed below.limit(number, optional)cursor(string, optional)orderBy(string, optional)order(asc|desc, optional)
Returns:
rows(table | nil): array of{ key, value, meta }.page(table | nil): paging info (nextCursor,count).err(nil| string)
Storage.Increment​
S Server Only
local value, meta, err = Storage.Increment(bucket, key, delta, options)
Parameters:
bucket(string)key(string)delta(number): positive or negative.options(table, optional): optional fields listed below.expectedRevision(number, optional)auditReason(string, optional)
Returns:
value(number | nil): updated numeric value.meta(table | nil)err(nil| string)
LocalStorage Functions​
LocalStorage.Get​
C Client Only
local value, err = LocalStorage.Get(namespace, key)
Parameters:
namespace(string): local namespace (for exampleui.chat).key(string)
Returns:
value(table | number | string | boolean | nil)err(nil| string)
LocalStorage.Set​
C Client Only
local err = LocalStorage.Set(namespace, key, value)
Parameters:
namespace(string)key(string)value(table | number | string | boolean)
Returns:
err(nil| string)
LocalStorage.Delete​
C Client Only
local err = LocalStorage.Delete(namespace, key)
Parameters:
namespace(string)key(string)
Returns:
err(nil| string)
LocalStorage.List​
C Client Only
local rows, err = LocalStorage.List(namespace, options)
Parameters:
namespace(string)options(table, optional): optional fields listed below.limit(number, optional)cursor(string, optional)
Returns:
rows(table | nil): array of{ key, value }.err(nil| string)
Metadata Shape​
Storage metadata fields:
version(number)revision(number)createdAt(number, ms epoch)updatedAt(number, ms epoch)updatedBy(string)
Security and Trust Rules​
Storageis authoritative. ACL enforcement happens server-side.LocalStorageis untrusted. Never use it for bans, permissions, economy, or ownership.- Client scripts cannot bypass ACL by writing local data.
- Use one permission system: ACL (
acl.json) for authorization rules; DB stores identity/data only.
Identity Strategy​
Use persistent identity keys for gameplay data:
- preferred:
accountId(logged-in identity) - fallback:
clientId(when account is not available yet) - avoid: session
playerIdfor persistent records
Recommended key prefixes:
account.<accountId>.*client.<clientId>.*for low-trust telemetry/preferences only
Suggested Key Design​
player.profile:<accountId>(orclient:<clientId>for guest-only flows)player.ban:<accountId>player.ban_client:<clientId>chat.mute:<accountId>economy.balance:<accountId>:<currency>resource.<resourceName>:<key>
Error Codes​
ERR_INVALID_BUCKETERR_INVALID_KEYERR_INVALID_VALUEERR_NOT_FOUNDERR_REVISION_CONFLICTERR_PERMISSION_DENIEDERR_QUOTA_EXCEEDEDERR_RATE_LIMITERR_SERIALIZATIONERR_STORAGE_UNAVAILABLEERR_TIMEOUT
Quick Examples​
Server record set/get:
local accountId = "a_abc123"
local clientId = "c_9e2d1a7f"
local meta, err = Storage.Set("player_bans", "player.ban:" .. accountId, {
accountId = accountId,
clientId = clientId,
isActive = true,
reason = "cheat_detected"
}, { auditReason = "manual moderation action" })
if err then
return Logger.Error("ban set failed: " .. err)
end
local value, meta2, getErr = Storage.Get("player_bans", "player.ban:" .. accountId)
if getErr then
return Logger.Error("ban get failed: " .. getErr)
end
Client local preference:
local err = LocalStorage.Set("ui.chat", "activeTab", "global")
if err then
return print("local save failed: " .. err)
end
local tab, getErr = LocalStorage.Get("ui.chat", "activeTab")
if not getErr then
print("active chat tab: " .. tostring(tab))
end
Economy example (server owner custom currency):
local accountId = "a_9e2d1a7f"
local key = "economy.balance:" .. accountId .. ":cash"
local value, meta, err = Storage.Increment("economy_balances", key, 250, {
auditReason = "job payout"
})
Optimistic update with revision check:
local current, meta, getErr = Storage.Get("economy_balances", key)
if not getErr then
local nextValue, nextMeta, updateErr = Storage.Update("economy_balances", key, function(v)
local row = v or { balance = 0 }
row.balance = (row.balance or 0) + 100
return row
end, {
expectedRevision = meta.revision,
auditReason = "daily reward"
})
end
Client local namespace listing:
local rows, err = LocalStorage.List("ui.chat", { limit = 50 })
if not err then
for _, row in ipairs(rows) do
print(row.key .. "=" .. tostring(row.value))
end
end