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:

  1. In Studio, insert a ModuleScript inside ServerScriptService.
  2. Rename it to Crux.
  3. Paste the full contents of Crux.lua into 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

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.