Roblox Crux Quickstart: Lua SDK, Data & Leaderboards
Connect Roblox to Crux from server-side Lua: install the module, verify players, save persistent data, submit leaderboard scores and manage economy.
This guide walks you from zero to a working Roblox server script that saves player data, submits leaderboard scores, and manages virtual economy - all through Crux, an external backend with its own documented quotas and a project-scoped data model that can be shared by multiple Roblox experiences.
Everything in this guide runs from a server-side Script. The Crux module uses HttpService for the external API calls, so the server token never needs to ship to a LocalScript.
What you'll have at the end: a Roblox server script that verifies players, reads and writes persistent data with optimistic locking, submits scores to a live leaderboard, and credits in-game currency - all backed by an external database your team can operate from a dashboard.
Prerequisites
- A Roblox experience with Allow HTTP Requests enabled (Game Settings → Security).
- A Crux account and a project with at least one environment. Create one here.
- A Server Token from your project's Tokens tab. This is what your Roblox server uses to authenticate with Crux. Keep it secret - never put it in a LocalScript.
Step 1 - Enable HttpService
In Roblox Studio, open Experience Settings → Security and turn on Allow HTTP Requests. Keep the integration in server-side Scripts: Roblox DataStoreService is server-side too, and your Crux server token is a trusted credential that must not be exposed to clients.
Security note: HTTP requests made through HttpService happen on your Roblox game server, not on players' machines. The server token never leaves your server scripts.
Step 2 - Install the Crux Module
Download Crux.lua from the public Roblox SDK source and add it to your experience:
- In Studio, insert a ModuleScript inside ServerScriptService.
- Rename it to
Crux. - Paste the full contents of
Crux.luainto the script body.
The module is self-contained - no external packages, no toolchain. One paste and it is ready.
Step 3 - Initialize the Client
In any server-side Script (not a LocalScript), require the module and call Crux.init with your project credentials:
-- ServerScriptService/GameServer (Script)
local Crux = require(game.ServerScriptService.Crux)
local gsb = Crux.init(
"YOUR_PROJECT_ID", -- e.g. "proj_01abc..."
"YOUR_SERVER_TOKEN", -- e.g. "gsb_st_..."
"YOUR_ENVIRONMENT_ID" -- e.g. "env_01abc..." (use your "production" env)
)
You can find all three values in your project dashboard under Settings → Environments and Settings → Tokens.
Step 4 - Verify Players on Join
When a player joins, call gsb:VerifyPlayer with their Roblox UserId. This creates a Crux player record the first time, or returns the existing one on subsequent joins. The returned player_id is what you use for all subsequent API calls.
local Players = game:GetService("Players")
-- Maps Roblox UserId → Crux player_id for this session
local playerIds = {}
Players.PlayerAdded:Connect(function(player)
local ok, result = pcall(function()
return gsb:VerifyPlayer(player.UserId)
end)
if ok then
playerIds[player.UserId] = result.player_id
print("[Crux] Verified " .. player.Name .. " → " .. result.player_id)
else
warn("[Crux] Verify failed for " .. player.Name .. ": " .. tostring(result))
end
end)
Players.PlayerRemoving:Connect(function(player)
playerIds[player.UserId] = nil
end)
Step 5 - Save and Load Player Data
GetDataStore() deliberately mirrors the small part of Roblox DataStoreService most game code needs: GetAsync, SetAsync, UpdateAsync, IncrementAsync, and RemoveAsync, but it is not a drop-in implementation of every Roblox DataStore semantic. Crux stores versioned JSON documents behind its own API and quotas. In the helper, SetAsync is an unconditional write; UpdateAsync reads the current version and retries a versioned write on conflict.
Basic get / set
-- Get a player's save data
local profileStore = gsb:GetDataStore("profile")
local function loadProfile(robloxUserId)
local playerId = playerIds[robloxUserId]
if not playerId then return end
local data, version = profileStore:GetAsync(playerId)
-- data is a Lua table (or nil if no save exists yet)
return data, version
end
local function saveProfile(robloxUserId, data)
local playerId = playerIds[robloxUserId]
if not playerId then return end
profileStore:SetAsync(playerId, data)
end
Safe updates with optimistic locking
For writes that must not overwrite concurrent changes - such as incrementing XP when multiple servers could write the same player - use UpdateAsync. It fetches the current version, passes the value to your transform function, and retries automatically if another server wrote first.
local function addXP(robloxUserId, amount)
local playerId = playerIds[robloxUserId]
if not playerId then return end
profileStore:UpdateAsync(playerId, function(current)
current = current or { xp = 0, level = 1 }
current.xp = current.xp + amount
-- level up every 1000 XP
current.level = math.floor(current.xp / 1000) + 1
return current
end)
end
Comparison: DataStore vs Crux
| Feature | Roblox DataStore | Crux Roblox helper |
|---|---|---|
| API shape | Native DataStoreService, including metadata/version APIs | Familiar subset: Get / Set / Update / Increment / Remove over player JSON documents |
| Request budgets | Documented experience- and server-level budgets; UpdateAsync consumes read + write budget |
Separate Crux API/plan quotas; compare them with your workload rather than assuming one side is always higher |
| Sharing scope | Shared by places/servers inside the same Roblox experience | Can deliberately share one Crux project across separate Roblox experiences you control |
| Operations | Creator Hub Data Stores Manager plus Open Cloud APIs | Crux dashboard + HTTP API for player documents and adjacent backend services |
| Concurrent update helper | UpdateAsync coordinates updates within DataStoreService |
UpdateAsync uses the document version and retries conflicts; SetAsync stays unconditional |
Step 6 - Leaderboards
First, create a leaderboard in your project dashboard (Leaderboards → New). Give it a name and set the sort order (descending for high-score, ascending for fastest time). Then use the Crux client to submit and read scores:
-- Submit a score when a round ends
local function onRoundEnd(robloxUserId, finalScore)
local playerId = playerIds[robloxUserId]
if not playerId then return end
gsb:SubmitScore("weekly-kills", playerId, finalScore, {
-- optional metadata attached to this entry
weapon = "shotgun",
mode = "tdm",
})
end
-- Show the top 10 at game start (e.g. to populate a lobby board)
local function showLeaderboard()
local entries = gsb:GetLeaderboardTop("weekly-kills", 10)
for _, entry in ipairs(entries) do
print(string.format("#%d %s %d", entry.rank, entry.player_id, entry.score))
end
end
-- Show where a specific player ranks
local function showPlayerRank(robloxUserId)
local playerId = playerIds[robloxUserId]
local standing = gsb:GetPlayerStanding("weekly-kills", playerId)
if standing then
print("You are rank " .. standing.rank .. " with " .. standing.score .. " kills")
end
end
Seasonal resets: configure a reset schedule (daily / weekly / monthly) in the dashboard. Crux archives the previous period automatically and starts a fresh board - no script changes needed.
Step 7 - Economy
Crux provides server-authoritative currency balances and item inventories. All adjustments are atomic - you can credit gold and add items in a single call without worrying about partial failures.
Read a player's economy
local function getEconomy(robloxUserId)
local playerId = playerIds[robloxUserId]
if not playerId then return end
local economy = gsb:GetPlayerEconomy(playerId)
-- currency_id/item_id are legacy SDK names for the stable catalog key.
for _, bal in ipairs(economy.balances or {}) do
print(bal.currency_name .. " (" .. bal.currency_id .. "): " .. bal.amount)
end
for _, item in ipairs(economy.inventory or {}) do
print(item.item_name .. " (" .. item.item_id .. ") ×" .. item.quantity)
end
end
Award currency and items on a kill
-- Use the stable catalog keys you configured in the dashboard.
local GOLD_KEY = "gold"
local CRATE_KEY = "crate"
local function onKillReward(robloxUserId)
local playerId = playerIds[robloxUserId]
if not playerId then return end
local updated = gsb:AdjustEconomy(
playerId,
{{ currency_key = GOLD_KEY, amount = 50 }},
{{ item_key = CRATE_KEY, quantity = 1 }}
)
return updated -- the resulting economy state
end
Step 8 - Matchmaking: Current Roblox SDK Boundary
Do not call the Roblox module's JoinMatchmaking, GetMatchStatus, or LeaveMatchmaking methods from this server-token setup yet. The current matchmaking queue identifies the player from a player access token; Crux.lua authenticates with a server token. The SDK keeps those methods for forward compatibility, but today they return 401 player authentication required.
Do not solve that by moving the server token into a LocalScript. For Roblox today, keep your existing queue/teleport flow or use your own trusted backend orchestration. Crux's server registry is usable from the server-token SDK for discovery, while server-on-behalf matchmaking needs a dedicated platform endpoint before this tutorial can honestly present it as runnable.
Putting It All Together
A minimal but complete server script for a session-based game:
-- ServerScriptService/GameServer
local Crux = require(game.ServerScriptService.Crux)
local Players = game:GetService("Players")
local gsb = Crux.init("YOUR_PROJECT_ID", "YOUR_SERVER_TOKEN", "YOUR_ENVIRONMENT_ID")
local GOLD_KEY = "gold"
local playerIds = {}
Players.PlayerAdded:Connect(function(player)
local ok, result = pcall(gsb.VerifyPlayer, gsb, player.UserId)
if not ok then return end
local pid = result.player_id
playerIds[player.UserId] = pid
-- Load profile
local ds = gsb:GetDataStore("profile")
local profile = ds:GetAsync(pid) or { xp = 0, level = 1, kills = 0 }
-- Store in player attribute for easy access
player:SetAttribute("gsb_pid", pid)
player:SetAttribute("xp", profile.xp)
player:SetAttribute("level", profile.level)
player:SetAttribute("kills", profile.kills)
end)
Players.PlayerRemoving:Connect(function(player)
local pid = player:GetAttribute("gsb_pid")
if not pid then return end
-- Save profile on leave
local ds = gsb:GetDataStore("profile")
ds:SetAsync(pid, {
xp = player:GetAttribute("xp"),
level = player:GetAttribute("level"),
kills = player:GetAttribute("kills"),
})
playerIds[player.UserId] = nil
end)
-- Called by your combat system
local function onPlayerKill(killerUserId)
local pid = playerIds[killerUserId]
if not pid then return end
-- Award gold and increment kill counter
gsb:AdjustEconomy(pid, {{ currency_key = GOLD_KEY, amount = 25 }}, {})
local killer = Players:GetPlayerByUserId(killerUserId)
if killer then
local kills = (killer:GetAttribute("kills") or 0) + 1
killer:SetAttribute("kills", kills)
gsb:SubmitScore("all-time-kills", pid, kills)
end
end
Cross-Experience Data Sharing
Roblox DataStores are shared between places inside the same experience. If you operate separate Roblox experiences and intentionally point their trusted server scripts at the same Crux project/environment, Crux can provide a common player/data namespace. Verify the Roblox user in each experience before using the returned Crux player id.
-- Experience A (e.g. your main game)
local gsbA = Crux.init("proj_shared", "st_main_game", "env_prod")
local verifiedA = gsbA:VerifyPlayer(player.UserId)
-- Experience B (e.g. a separate hub experience)
local gsbB = Crux.init("proj_shared", "st_hub", "env_prod")
local verifiedB = gsbB:VerifyPlayer(player.UserId)
local pid = verifiedB.player_id
local profile = gsbB:GetDataStore("profile"):GetAsync(pid)
Troubleshooting
| Error | Likely Cause | Fix |
|---|---|---|
HttpService is not enabled |
HTTP requests disabled in game settings | Game Settings → Security → Allow HTTP Requests |
API Error (401) |
Wrong or expired server token | Check token in dashboard; make sure you're using the correct environment |
API Error (404) |
Wrong project ID, environment ID, or resource doesn't exist yet | Double-check IDs; create the leaderboard or currency in the dashboard first |
API Error (429) |
Crux API/plan rate limit reached | The SDK retries automatically with backoff. Check your plan limits. |
No data returned from GetAsync |
Player not verified yet, or document doesn't exist | Call VerifyPlayer first; treat nil return as empty initial state |
Next Steps
- Dashboard player browser - search any player by Crux ID, view and edit their documents, see economy state, inspect audit logs. No script needed.
- Seasonal leaderboards - configure a reset schedule in the dashboard. Crux archives the previous period automatically.
- Server browser - use
gsb:GetServers()from trusted Roblox server code to query the Crux registry. This is available with the server-token SDK today. - Around-player leaderboard -
gsb:GetPlayersAroundPlayer()gives the local rank window without downloading the whole board. - Config delivery -
gsb:GetActiveConfig()returns the active bundle as a raw string. Decode JSON yourself when that is your bundle format:local config = game:GetService("HttpService"):JSONDecode(gsb:GetActiveConfig()).
Related in This Hub
- Roblox HttpService for External Backends
- Roblox DataStore vs External Database
- Roblox Cross-Experience Progression
- Seasonal Leaderboards and Game Economy
- Player Authentication for Dedicated-Server Games
- Game Server Backend hub
To create a project and grab your credentials, visit Crux.
Sources
Technical, pricing, and product claims were checked against these primary sources on the verification date above.