Connect your Roblox game to Baseplate
Baseplate is a single open-source server module with an embedded client helper. There are no external dependencies, no remote code loading, and no client-side tokens.
Quick start (2 minutes)
- Create a game in Add a game and copy your
bpt_...token. - Install the Baseplate Connect Studio plugin and paste the one-step setup code — or download Baseplate.rbxmx and insert it into
ServerScriptService. - Enable Game Settings → Security → Allow HTTP Requests.
- Add a server
ScriptinServerScriptServicethat callsBaseplate.start. - Press Play in Studio. Your setup page will turn green within 15 seconds.
1. Add the module to your game
Option A — Baseplate Connect Studio plugin (one step): save BaseplateConnect.rbxmx into your Roblox Studio Plugins folder, click Baseplate Connect in the Plugins toolbar, and paste the one-step code from your game's Setup page. It drops the Baseplate module and a ready-made BaseplateLoader script into ServerScriptService, turns on HttpService.HttpEnabled, and uploads your 3D place map.
Option B — Manual .rbxmx file: download Baseplate.rbxmx, right-click ServerScriptService in Roblox Studio → Insert from File…, and pick the downloaded file. You should see a ModuleScript named Baseplate (with a Client LocalScript nested inside it).
Leave Client inside Baseplate — when Baseplate.start() runs, it automatically copies Client into StarterPlayerScripts so client FPS, device type, idle time, the Roblox Escape menu, and open ScreenGui windows are reported.
2. Allow HTTP requests
In Roblox Studio open File → Game Settings → Security and turn on Allow HTTP Requests. (If your place isn't published yet, save it to Roblox first, or run game:GetService("HttpService").HttpEnabled = true in the Command Bar.)
Recommended for production: store your token in Creator Hub → Creations → [Your Experience] → Settings → Secrets with the name baseplate_token and read it via HttpService:GetSecret("baseplate_token") so the raw key never sits in plain text inside your place file.
3. Start Baseplate
Create a Script in ServerScriptService:
local HttpService = game:GetService("HttpService")
local Baseplate = require(game.ServerScriptService.Baseplate)
Baseplate.start({
token = HttpService:GetSecret("baseplate_token"),
studio = true, -- send data from Studio playtests too (kept separate from live)
})Or, when testing quickly in Studio, pass the token string directly:
local Baseplate = require(game.ServerScriptService.Baseplate)
Baseplate.start({
token = "bpt_your_token_here",
studio = true,
})All Baseplate.start(options)
| Option | Type | Default | Description |
|---|---|---|---|
token | string | Secret | required | Your game token (bpt_...) or HttpService:GetSecret("baseplate_token"). |
studio | boolean | false | When false, the SDK does nothing inside Roblox Studio. Set to true while testing your integration. |
debug | boolean | false | Prints queue & batch status messages to the Studio Output window. |
client | boolean | true | Installs the lightweight client script that reports client FPS, device type, idle time, the Escape menu, and open GUI windows. |
deaths | "auto" | "manual" | "auto" | Automatically listens to Humanoid.Died. Set to "manual" if your game uses custom knockouts and you call Baseplate.death(player, cause) yourself. |
firstJoinStore | boolean | true (live) | Uses a tiny DataStore key (BaseplateSDK_FirstJoin_v1) once per player so is_new accurately distinguishes first-time visitors from returning players. |
Studio playtests vs. live data
Sessions recorded from Roblox Studio (studio = true) are flagged on ingest. They are hidden from all charts by default so your playtests never pollute retention, revenue, or leave-reason numbers. Flip the Studio toggle in the top bar of the dashboard whenever you want to inspect your Studio test sessions.
What's tracked automatically
With only Baseplate.start in place — and zero extra lines of code — you get:
API reference (all optional)
Call these from any server Script after Baseplate.start. Every function is safe to call with invalid or nil arguments — it will never throw or yield your game thread.
Baseplate.event(player, name, fields?)
Record any custom event in your game (alias: Baseplate.track). fields is an optional table of up to 16 key/value pairs (strings, numbers, booleans). Also appears in the player's last-15-actions trail and on the Events tab.
Baseplate.event(player, "QuestComplete", {
quest = "FindTheKey",
seconds = 42,
hard_mode = true,
})
-- Server-wide event (no player):
Baseplate.event(nil, "BossSpawned", { boss = "Dragon", level = 5 })Baseplate.step(player, stepName)
Mark that a player reached a step in an onboarding flow or progression funnel. On the Why players leave → Funnels tab you can build a funnel from any ordered list of step names and see the exact drop-off between steps.
Baseplate.step(player, "1_Spawned")
Baseplate.step(player, "2_OpenedShop")
Baseplate.step(player, "3_BoughtStarterSword")
Baseplate.step(player, "4_EnteredFirstDungeon")Baseplate.tutorial(player, stepNumber)
Tell Baseplate which tutorial step a player is currently on (1, 2, 3, …). When they complete the tutorial, call Baseplate.tutorial(player, 0). If a player leaves while stepNumber > 0, their session is automatically classified as Quit in tutorial with that step number.
Baseplate.tutorial(player, 1) -- started step 1
Baseplate.tutorial(player, 2) -- reached step 2
Baseplate.tutorial(player, 0) -- finished!Baseplate.currency(player, currency, amount, source?)
Track soft-currency flow. Pass a positive amount when a player earns currency and a negative amount when they spend it. Powers the Economy tab (sources vs. sinks, inflation balance, and players stuck with no earnings).
Baseplate.currency(player, "Coins", 100, "QuestReward")
Baseplate.currency(player, "Coins", -50, "BuySword")
Baseplate.currency(player, "Gems", 5, "DailyBonus")Baseplate.receipt(receiptInfo)
Game Pass purchases are tracked automatically. For Developer Products, Roblox requires a single MarketplaceService.ProcessReceipt callback per game — add one line inside yours (deduplicated by PurchaseId):
local MarketplaceService = game:GetService("MarketplaceService")
MarketplaceService.ProcessReceipt = function(receiptInfo)
-- your code that grants the item to the player...
Baseplate.receipt(receiptInfo)
return Enum.ProductPurchaseDecision.PurchaseGranted
endBaseplate.variant(player, testName, variants) → string
Deterministically assigns a player to an A/B test variant (the same UserId always gets the same variant for a given testName across sessions) and logs the exposure for the A/B Tests tab.
-- 50 / 50 split:
local group = Baseplate.variant(player, "starter_pack", { "control", "cheaper" })
if group == "cheaper" then
-- show the 49 Robux price
else
-- show the 99 Robux price
end
-- Custom weights (80% / 20%):
local layout = Baseplate.variant(player, "shop_layout", { old = 80, new = 20 })Baseplate.loaded(player)
By default, a player is marked as loaded when their character first spawns. If your game has a custom loading screen or DataStore profile load before gameplay starts, call Baseplate.loaded(player) when loading finishes so players who quit earlier are accurately tagged as Left while loading.
-- After DataStore profile is ready and character is spawned:
Baseplate.loaded(player)Baseplate.mark(player, label)
Adds a lightweight breadcrumb to the player's last-15-actions trail without creating a full analytics event.
Baseplate.mark(player, "OpenedShop")
Baseplate.mark(player, "StartedBossFight")Baseplate.death(player, cause?)
Records a player death with an optional cause label (e.g. "Lava", "BossSlam") and the player's current map position. If your game uses custom knockouts instead of Humanoid.Died, pass deaths = "manual" to Baseplate.start and call Baseplate.death yourself:
Baseplate.start({
token = HttpService:GetSecret("baseplate_token"),
deaths = "manual",
})
-- Inside your combat / health system:
Baseplate.death(player, "Lava")
Baseplate.death(player, "SkeletonKing")Baseplate.kick(player, reason?)
Marks the session as kicked with the given reason and calls player:Kick(reason), so the session's leave reason is recorded as Kicked by server instead of a normal quit.
Baseplate.kick(player, "AFK timeout")Baseplate.status() → table
Returns the current queue and HTTP state for debugging in Studio:
print(Baseplate.status())
-- { started = true, queued = 3, dropped = 0, sent = 4, lastStatus = 202 }How leave reasons work
When a player leaves, Baseplate evaluates the session from top to bottom and assigns the first matching reason:
| # | Reason | When it triggers |
|---|---|---|
| 1 | server_shutdown | The server shut down while the player was still connected. |
| 2 | kicked | Baseplate.kick(player, reason) was called. |
| 3 | left_loading | Player left before their character spawned or before Baseplate.loaded(player) ran. |
| 4 | bounced | Session lasted under 60 seconds. |
| 5 | declined_purchase | Player closed a Robux purchase prompt without buying within the last 120 seconds before leaving. |
| 6 | quit_tutorial | Player left while Baseplate.tutorial(player, step) was still > 0. |
| 7 | afk | No input for 3+ minutes before leaving. |
| 8 | poor_performance | Average client FPS below 20 or average ping above 450 ms. |
| 9 | script_errors | 3 or more script errors occurred during the session. |
| 10 | roblox_menu | Player opened the Roblox Escape menu and clicked Leave. |
| 11 | gui_open | Player left while an in-game GUI window was open. |
| 12 | long_session | Played for 30+ minutes with none of the friction signals above. |
| 13 | normal | Normal session between 1 and 30 minutes with no friction signals. |
| 14 | server_lost | The Roblox server stopped reporting without a clean shutdown (crash). |
Keys & team access
- Universe binding: on the first batch from a live Roblox server, your game is automatically locked to that
game.GameId(Universe ID). Batches from any other universe are rejected with403. - Rotating tokens: open Settings → Keys on your game dashboard to create a new token and revoke old ones at any time.
- Team access: open Settings → Team to create a one-time invite link for collaborators as a Member or read-only Viewer.
Discord & Telegram alerts
In Settings → Alerts you can connect a Discord webhook or Telegram bot to receive automatic notifications (checked every 5 minutes, with a 2-hour cooldown per alert type):
| Alert | When it fires |
|---|---|
| Online drop | Live players drop by more than 50% vs. the previous hour (or vs. the same hour yesterday). |
| Error burst / new error | 20+ errors in 15 minutes at 3× your 24h baseline, or a brand-new error fingerprint in the latest place version. |
| Servers lost | 3 or more active servers go silent within 15 minutes without a clean shutdown. |
| Leave reason surge | A friction leave reason (bounced, loading, tutorial, declined purchase, poor FPS, script errors) doubles its 7-day baseline share over the last 2 hours. |
| New version & 24h report | When a new PlaceVersion goes live, plus an automatic 24-hour before/after comparison report. |
| Daily summary | Once per day at your chosen UTC hour: players, new players, peak CCU, avg session, Robux, and top leave reason. |
Update impact
Every session and event is automatically tagged with game.PlaceVersion. On the Updates tab, Baseplate compares each version against the previous one using per-player rates (avg session length, playtime per player, bounce %, errors per 100 sessions, avg FPS, payer conversion %, Robux per player, and D1 retention) so a newly released version with fewer total hours is compared fairly against an older version.
Why it moved
Click any headline metric card on Overview (Active players, Playtime, Avg session, Robux, or D1 retention) to open Why it moved — an automatic breakdown comparing the current period against the previous period across device mix, new vs. returning players, join sources, countries, leave reasons, place version releases, script errors, and payer conversion vs. ARPPU.
Annotations & CSV export
Add a note below the Overview chart whenever you ship an update, run an event, or launch an ad campaign — Baseplate marks it on the timeline and calculates the 24-hour before/after delta. Every table and chart also has a CSV download button.
Data & privacy
- What is stored: within your game, Baseplate records each player's Roblox UserId, username, and display name so you can inspect individual sessions and player histories, alongside country code, device type, FPS/ping, session times, leave reason, purchases, and your custom events.
- What is never collected: Baseplate never reads chat messages, voice, passwords, real names, email addresses, IP addresses, or
.ROBLOSECURITYcookies, and never links player profiles across different games. - Roblox Right to Erasure: open Settings → Privacy on your game dashboard and enter any Roblox
UserId(or connect a Roblox Right to Erasure webhook) to permanently delete all sessions, events, and summary rows for that player.
Data retention
| Data | Free | Pro |
|---|---|---|
| Raw events & heatmap points | 7 days | 30 days |
| Individual sessions | 30 days | 90 days |
| Daily totals, retention cohorts & player summaries | Kept for the life of your game | Kept for the life of your game |
Limits & budget
- Roblox HttpService budget: at most 6 HTTP requests per minute per Roblox server (~1.2% of Roblox's 500 req/min limit) — 4 lightweight presence pings + up to 2 event batches.
- Batch size: up to 500 events or 240 KB per batch; the in-memory queue holds up to 6,000 events and drops the oldest if a server is offline for an extended period.
- Custom event fields: up to 16 fields per event; field names are
snake_case(max 32 chars) and string values are truncated at 200 chars.
Troubleshooting
| Symptom | Fix |
|---|---|
[Baseplate] not started: studio is off | You are playtesting in Studio with the default studio = false. Pass studio = true to Baseplate.start while testing. |
HttpService is not allowed to access ROBLOX resources / HttpEnabled | Turn on File → Game Settings → Security → Allow HTTP Requests in Roblox Studio. |
lastStatus = 401 | The token doesn't match an active key. Copy a fresh token from Settings → Keys. |
lastStatus = 403 (universe_mismatch) | This token is already bound to a different Roblox Universe ID. Create a separate game entry for each Roblox experience. |
| Setup page is green, but Overview charts show 0 | Studio playtests are hidden by default. Click the Studio button in the top bar of the dashboard to include Studio data. |
AI & CLI (MCP)
Baseplate ships with an official CLI and MCP server so you can ask Claude Desktop, Claude Code, Cursor, Windsurf, ChatGPT, or your terminal questions about your own games and public Roblox charts. Open AI & CLI in the dashboard for a ready-to-paste config and a personal bpt_agent_... token.
npx baseplate login
npx baseplate games
npx baseplate live <gameId>
npx baseplate overview <gameId> --range 7dHTTP ingest (for custom clients)
The Luau module sends JSON batches to POST /v1/ingest with Authorization: Bearer bpt_... and lightweight 15-second presence pings to POST /v1/presence:
{
"v": 1,
"sdk": "0.10.3",
"game": 1234567890,
"place": 9876543210,
"job": "server-job-uuid",
"ver": 42,
"seq": 1,
"dropped": 0,
"ids": {
"player_token": "{\"u\":123456,\"n\":\"Player1\",\"d\":\"Player\"}"
},
"rows": [
{
"t": 1761560000,
"e": "session_start",
"p": "player_token",
"s": "session_id",
"q": 1,
"v": 42,
"j": "job8char",
"n": 12,
"d": { "is_new": true, "source": "friend", "country": "US" }
}
]
}