v3.0 · clerk auth Quick Start API Ref Examples
INTRODUCTION

RankPortal
Complete Guide

Everything you need to build a fully working Roblox group ranking system — from creating a Clerk account to ranking players live from your game server. One guide, nothing missing.

🔐 Clerk Auth ⚡ Node.js Backend ☁️ Railway Deploy 🎮 Roblox Studio 🤖 Bot Account
ℹ️

This guide covers the full stack: Clerk handles user login so only your authorized users can generate API keys. Those keys are then used by your Roblox games to perform ranking actions through your hosted server.


ARCHITECTURE

How Everything Connects

There are two separate flows — the portal flow (web users setting up their key) and the game flow (Roblox calling your API at runtime).

Portal Flow — Getting an API Key

🧑
User
Opens portal
🔐
Clerk Login
Sign up / Sign in
🍪
Paste Cookie
Bot .ROBLOSECURITY
🔑
API Key
rsp_xxxxx

Game Flow — Ranking a Player

🎮
Roblox Game
Lua module POST
🔑
API Key Check
authorization header
🖥️
Your Server
Express / Node.js
🌐
Roblox API
groups.roblox.com
💡

Roblox game servers can't call Roblox's own ranking API directly (security restriction). Your Express server acts as the authorized middleman, authenticated via the bot account's cookie.


BEFORE YOU START

Prerequisites

  • A Roblox account with owner/admin access to your group
  • A separate Roblox bot account (never use your main)
  • A Clerk.com account (free)
  • A GitHub account (free) for deployment
  • A Railway.app account (free tier available)
  • Roblox Studio installed on your computer
  • Basic understanding of Lua scripting

STEP 1

Create a Bot Account

The bot account is the Roblox account that performs all ranking actions. Roblox will show rank changes as coming from this account.

⚠️

Always use a dedicated bot account. Never use your main account. The bot's cookie grants full account access — if your server is ever compromised, you only lose the bot account.

  1. Register a new Roblox account

    Go to roblox.com and sign up. Use a descriptive name like YourGroupRankBot. Use a unique email (a Gmail alias works).

  2. Verify the email

    Check your inbox and verify. Unverified accounts may get flagged when making API calls.

  3. Log in as the bot and join your group

    Visit your group page while logged in as the bot and click Join. Accept from your main account if approval is required.

  4. Promote the bot to a rank with permissions

    From your main account go to Group → Members → find the bot → assign a rank. The bot can only set ranks lower than its own, so make sure it sits high enough.

💡

Rank hierarchy rule: If your bot is Rank 200, it can only assign ranks 1–199. Place the bot above all ranks you want to set programmatically.

STEP 3

Set Group Permissions

The bot's group role needs specific permissions enabled. Go to your Group → Configure GroupRoles → edit the bot's role.

REQUIRED PERMISSIONS FOR BOT ROLE
✅  Manage lower-ranked members  → required for setRank, exile
✅  Remove members               → required for exile
✅  Post group shouts            → required for shout
☐   Invite members               → optional, for join requests

Save the role. Changes apply immediately — no server restart needed.


STEP 4

Create a Clerk Application

Clerk handles all user authentication for the portal. It's free for up to 10,000 monthly active users and requires zero auth UI code.

  1. Sign up at clerk.com

    Go to clerk.com and create a free account.

  2. Click "Create application"

    From the Clerk dashboard, click Create application. Name it RankPortal.

  3. Choose sign-in methods

    Select Email + Password at minimum. You can also enable Google, Discord, or GitHub — Clerk handles the OAuth automatically. Click Create application.

  4. You're taken to the API Keys page

    Clerk shows you two keys immediately. Keep this tab open — you'll need them next.

STEP 5

Get Your Clerk API Keys

After creating your Clerk app, you'll see two keys on the API Keys page. You can always find them later at Configure → API Keys.

# Your keys look like these (examples):
CLERK_PUBLISHABLE_KEY = pk_test_c3VwZXItc2VhbC0yNS5jbGVyay5hY2NvdW50cy5kZXYk
CLERK_SECRET_KEY = sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
KeyPrefixWhere it goesSafe to expose?
Publishable Keypk_test_ / pk_live_Frontend HTML file✓ Yes — it's public
Secret Keysk_test_ / sk_live_Server .env file only✗ Never expose
🚫

Never put the Secret Key in HTML or commit it to GitHub. Only the Publishable Key goes in index.html. The Secret Key lives in .env on the server.

STEP 6

Configure Clerk Settings

Add Allowed Origins

In the Clerk Dashboard go to Configure → Restrictions → Allowed Origins and add:

ADD THESE ORIGINS
http://localhost:3000
https://YOUR-APP.up.railway.app

Set Redirect URLs

Go to Configure → Paths and set all redirect URLs to / — the portal is a single page app.

SettingValue
Sign-in URL/
Sign-up URL/
After sign-in URL/
After sign-up URL/

STEP 7

Install & Run the Server

BASH — Install dependencies
# Extract the project zip, then:
cd rankportal
npm install

# Copy the env template
cp .env.example .env

Open .env and fill in your Clerk keys:

.env
# Clerk — from dashboard.clerk.com → API Keys
CLERK_PUBLISHABLE_KEY=pk_test_YOUR_KEY_HERE
CLERK_SECRET_KEY=sk_test_YOUR_SECRET_HERE

# Server port
PORT=3000

Open public/index.html and replace the placeholder with your Publishable Key (around line 12):

HTML — public/index.html
data-clerk-publishable-key="pk_test_YOUR_PUBLISHABLE_KEY_HERE"

Start the server:

BASH
npm start
# or for development with auto-reload:
npm run dev

Open http://localhost:3000. You should see the portal with a Clerk-powered Sign In button.

REFERENCE

Environment Variables

VariableRequiredDescription
CLERK_PUBLISHABLE_KEYRequiredFrom Clerk Dashboard → API Keys. Also goes in index.html.
CLERK_SECRET_KEYRequiredFrom Clerk Dashboard → API Keys. Server-side only — never expose.
PORTOptionalHTTP port. Defaults to 3000 if not set.
STEP 8

Deploy to Railway

Your Roblox game needs a public HTTPS URL to call your server. Railway is free and takes about 5 minutes.

  1. Push code to GitHub

    Make sure .env and data/ are in .gitignore (they are by default).

    BASH
    git init && git add .
    git commit -m "initial commit"
    git remote add origin https://github.com/YOU/rankportal.git
    git push -u origin main
  2. Create project on Railway

    Go to railway.appNew ProjectDeploy from GitHub repo → select your repo.

  3. Add environment variables

    In Railway click Variables and add all three env vars from the table above.

  4. Get your public URL

    Railway gives you a URL like https://rankportal-production.up.railway.app. Copy it.

  5. Add the URL to Clerk's Allowed Origins

    Go back to Clerk Dashboard → Configure → Restrictions → Allowed Origins and add your Railway URL.


STEP 9

Set the Publishable Key in HTML

The Clerk browser SDK loads from a CDN via a <script> tag. The publishable key is passed as a data- attribute — Clerk's standard approach for plain JS apps.

HTML — public/index.html (near top of head)
<script
  async crossorigin="anonymous"
  data-clerk-publishable-key="pk_test_YOUR_KEY_HERE"
  src="https://cdn.jsdelivr.net/npm/@clerk/clerk-js@latest/dist/clerk.browser.js"
></script>
EnvironmentKey prefixUse when
Developmentpk_test_Running on localhost
Productionpk_live_Deployed on Railway / Render
STEP 10

Test the Portal

  1. Open the portal in a browser

    Visit http://localhost:3000 (or your Railway URL). You should see the landing page with a Sign In button.

  2. Sign up with a test account

    Click Sign In — Clerk's modal appears. Create an account with an email address. Clerk sends a verification code.

  3. Paste your bot cookie and generate a key

    After logging in you see the dashboard. Paste your .ROBLOSECURITY cookie and click Generate. You'll receive an API key like rsp_4a7f2e9d...

  4. Copy the key

    Click COPY and save it somewhere safe. You'll paste it into your Roblox scripts.

You can see all registered users in Clerk Dashboard → Users. Your API keys and cookies are stored in data/keys.json on the server.


STEP 11

Roblox Studio Setup

Enable HTTP Requests

  1. Open your game in Roblox Studio

    Launch Studio and open your place file.

  2. Go to Game Settings → Security

    Click Home → Game Settings in the top menu.

  3. Toggle "Allow HTTP Requests" to ON

    Without this, all HTTP calls will throw an error and nothing will work.

⚠️

HTTP Requests must be enabled per game. If you publish a new place, enable it again there too.

Script Placement Rules

The ranking module must run on the server. Only Script objects (not LocalScript) can make HTTP calls.

WHERE TO PLACE SCRIPTS
✅  ServerScriptService  → Script        (correct)
✅  ServerStorage        → Script        (correct)
❌  StarterPlayerScripts → LocalScript   (will NOT work)
❌  StarterGui           → LocalScript   (will NOT work)
STEP 12

Install the Ranking Module

  1. Create a ModuleScript in ServerStorage

    In the Explorer panel right-click ServerStorage → Insert Object → ModuleScript. Name it RankingService.

  2. Paste the full module code below

    LUA — RankingService ModuleScript (paste entire block)
    local HttpService = game:GetService("HttpService")
    local RunService  = game:GetService("RunService")
    
    -- /api in Studio, /roblox in live server
    local mainPath = RunService:IsStudio() and "/api" or "/roblox"
    
    local rankingService = {}
    local pendingUsers, rateLimit = {}, {}
    rankingService.__index = rankingService
    
    function rankingService.new(gameKey, host)
        local self = {
            host    = host or "https://YOUR-SERVER.up.railway.app",
            gameKey = gameKey,
        }
        setmetatable(self, rankingService)
        return self
    end
    
    function rankingService:makeRequest(path, method, data)
        local url     = string.format("%s%s%s", self.host, mainPath, path)
        local headers = { authorization = self.gameKey }
        local body    = nil
    
        local urlRL = rateLimit[url]
        if urlRL and urlRL > 0 then
            task.wait(urlRL - os.time())
        elseif method == "POST" then
            headers["Content-Type"] = "application/json"
            body = HttpService:JSONEncode(data)
        end
    
        local ok, response = pcall(function()
            return HttpService:RequestAsync({
                Url = url, Method = method, Headers = headers, Body = body
            })
        end)
    
        if not ok then
            warn("[RankingService]", response)
            if response == "Number of requests exceeded limit" then
                task.wait(30)
                return self:makeRequest(path, method, data)
            end
            return { success = false, error = response }
        end
    
        rateLimit[url] = nil
        if response.Headers["retry-after"] then
            local wait = os.time() + response.Headers["retry-after"]
            rateLimit[url] = wait
            task.wait(wait - os.time())
            return self:makeRequest(path, method, data)
        end
    
        local parsed
        local _, err = pcall(function()
            parsed = HttpService:JSONDecode(response.Body)
        end)
        if err then
            warn("[RankingService] JSON decode error:", err)
            return { success = false, error = response.Body }
        end
        return parsed
    end
    
    function rankingService:setRank(groupId, userId, rank)
        if pendingUsers[userId] then return { error = "Pending" } end
        pendingUsers[userId] = true
        local r = self:makeRequest("/setRank", "POST", { groupId=groupId, userId=userId, rank=rank })
        pendingUsers[userId] = false
        return r
    end
    
    function rankingService:exile(groupId, userId)
        if pendingUsers[userId] then return { error = "Pending" } end
        pendingUsers[userId] = true
        local r = self:makeRequest("/exile", "POST", { groupId=groupId, userId=userId })
        pendingUsers[userId] = false
        return r
    end
    
    function rankingService:handleJoinRequest(groupId, userId, accept)
        if pendingUsers[userId] then return { error = "Pending" } end
        pendingUsers[userId] = true
        local r = self:makeRequest("/handleJoinRequest", "POST", { groupId=groupId, userId=userId, accept=accept })
        pendingUsers[userId] = false
        return r
    end
    
    function rankingService:shout(groupId, message)
        return self:makeRequest("/shout", "POST", { groupId=groupId, message=message })
    end
    
    function rankingService:getRank(groupId, userId)
        return self:makeRequest("/getRank", "POST", { groupId=groupId, userId=userId })
    end
    
    return rankingService
  3. Create a RankConfig Script in ServerScriptService

    Right-click ServerScriptService → Insert Object → Script. Name it RankConfig. Paste and fill in your values:

    LUA — RankConfig Script
    -- RankConfig — Script inside ServerScriptService
    local RankingService = require(game.ServerStorage.RankingService)
    
    local ranking = RankingService.new(
        "rsp_YOUR_API_KEY_HERE",                  -- from the portal
        "https://YOUR-SERVER.up.railway.app"      -- your Railway URL
    )
    
    -- Expose globally so other scripts can use it
    _G.Ranking  = ranking
    _G.GROUP_ID = 12345678  -- your Roblox group ID
STEP 13

Your First Rank Action

Test the full pipeline end-to-end with this touch-to-rank script.

LUA — Script inside a Part in Workspace
local Players  = game:GetService("Players")
local part     = script.Parent
local debounce = {}

part.Touched:Connect(function(hit)
    local character = hit.Parent
    local player    = Players:GetPlayerFromCharacter(character)
    if not player or debounce[player.UserId] then return end
    debounce[player.UserId] = true

    -- Wait for RankConfig to be ready
    local t = 0
    while not _G.Ranking and t < 10 do task.wait(0.5); t += 0.5 end
    if not _G.Ranking then debounce[player.UserId] = nil; return end

    local result = _G.Ranking:setRank(_G.GROUP_ID, player.UserId, 5)

    if result.success then
        print("✅ Ranked", player.Name, "to rank 5")
    else
        warn("❌ Failed:", result.error)
    end

    task.wait(5)
    debounce[player.UserId] = nil
end)

See ✅ Ranked [name] to rank 5 in the Output window? The full pipeline is working. If you see an error, jump to the Error Reference.


REFERENCE

Ranking API Reference

All methods on the RankingService Lua module. Initialize once in RankConfig then call from any server Script.

LUA — Initialization
local RankingService = require(game.ServerStorage.RankingService)
local ranking = RankingService.new(apiKey, serverUrl)
POST/roblox/setRankSet a player's rank number

Changes the user's rank to the specified number. The bot must have a higher rank than the target rank.

ParamTypeRequiredDescription
groupIdnumberYesYour Roblox group ID
userIdnumberYesTarget player's UserId
ranknumberYesRank number 1–255 to assign
LUA
local result = ranking:setRank(groupId, player.UserId, 5)
if result.success then print("Ranked!") else warn(result.error) end
POST/roblox/exileRemove player from group

Removes (exiles) the user from the group. Requires the bot to have Remove Members permission.

ParamTypeRequiredDescription
groupIdnumberYesYour Roblox group ID
userIdnumberYesTarget player's UserId
LUA
local result = ranking:exile(groupId, player.UserId)
if result.success then print(player.Name, "exiled.") end
POST/roblox/handleJoinRequestAccept or deny a join request

Accepts or declines a pending group join request. Only works if your group uses manual approval mode.

ParamTypeRequiredDescription
groupIdnumberYesYour Roblox group ID
userIdnumberYesUserId of the requesting player
acceptbooleanYestrue to accept, false to deny
LUA
ranking:handleJoinRequest(groupId, player.UserId, true)  -- accept
ranking:handleJoinRequest(groupId, player.UserId, false) -- deny
POST/roblox/shoutPost a group shout

Updates the group's shout message. Max 255 characters. Roblox rate-limits this endpoint — don't call it too frequently.

ParamTypeRequiredDescription
groupIdnumberYesYour Roblox group ID
messagestringYesShout message (max 255 chars)
LUA
ranking:shout(groupId, "🏆 New server online! Come join!")
POST/roblox/getRankGet player's current rank

Returns the player's current rank number. Returns 0 if not in the group. Use this before setRank to avoid downgrading users.

ParamTypeRequiredDescription
groupIdnumberYesYour Roblox group ID
userIdnumberYesPlayer's UserId to check
LUA
local result = ranking:getRank(groupId, player.UserId)
if result.success then
    print(player.Name, "is rank", result.data)
end

REFERENCE

Portal API Reference

These endpoints are used by the portal frontend (require a Clerk session token in the x-rp-token header).

POST/portal/registerSubmit cookie, get API key

Validates the Roblox cookie and creates (or updates) an API key for the logged-in Clerk user.

BodyTypeDescription
cookiestringFull .ROBLOSECURITY cookie value
GET/portal/mykeyGet current key info

Returns the logged-in user's API key details, Roblox username, request count, and timestamps. Returns hasKey: false if no key exists yet.

DELETE/portal/mykeyRevoke key

Permanently deletes the logged-in user's API key and stored cookie. Irreversible — they'll need to generate a new key.


EXAMPLES

Common Patterns

Rank on GamePass purchase

LUA — Auto-promote GamePass owners on join
local MarketplaceService = game:GetService("MarketplaceService")
local Players           = game:GetService("Players")
local VIP_PASS_ID = 123456789
local VIP_RANK    = 50

Players.PlayerAdded:Connect(function(player)
    player.CharacterAdded:Wait()
    task.wait(1)
    while not _G.Ranking do task.wait(0.5) end

    local ok, hasPass = pcall(function()
        return MarketplaceService:UserOwnsGamePassAsync(player.UserId, VIP_PASS_ID)
    end)

    if not ok or not hasPass then return end

    local current = _G.Ranking:getRank(_G.GROUP_ID, player.UserId)
    if current.success and current.data < VIP_RANK then
        _G.Ranking:setRank(_G.GROUP_ID, player.UserId, VIP_RANK)
        print("Promoted VIP:", player.Name)
    end
end)

Rank based on score / kills

LUA — Tiered promotion by score
local function checkPromotion(player, kills)
    local targetRank =
        kills >= 100 and 50 or
        kills >=  50 and 30 or
        kills >=  25 and 15 or
        kills >=  10 and  5 or nil

    if not targetRank then return end

    local current = _G.Ranking:getRank(_G.GROUP_ID, player.UserId)
    if current.success and current.data < targetRank then
        local res = _G.Ranking:setRank(_G.GROUP_ID, player.UserId, targetRank)
        if res.success then
            print("🎉 Promoted", player.Name, "to rank", targetRank)
        end
    end
end
ADVANCED

Advanced Examples

Admin chat commands

LUA — !rank and !exile chat commands
local Players = game:GetService("Players")
local ADMINS  = { [123456789] = true }  -- your UserId

-- Commands: !rank USERNAME RANKNUM | !exile USERNAME

Players.PlayerAdded:Connect(function(player)
    player.Chatted:Connect(function(msg)
        if not ADMINS[player.UserId] then return end
        local parts = string.split(msg, " ")
        local cmd   = string.lower(parts[1])

        if cmd == "!rank" and #parts == 3 then
            local target = Players:FindFirstChild(parts[2])
            local rankN  = tonumber(parts[3])
            if target and rankN then
                local r = _G.Ranking:setRank(_G.GROUP_ID, target.UserId, rankN)
                print(r.success and "Ranked!" or r.error)
            end

        elseif cmd == "!exile" and #parts == 2 then
            local target = Players:FindFirstChild(parts[2])
            if target then
                _G.Ranking:exile(_G.GROUP_ID, target.UserId)
            end
        end
    end)
end)

Shout when a new server opens

LUA — Server-open shout
task.spawn(function()
    while not _G.Ranking do task.wait(0.5) end
    _G.Ranking:shout(_G.GROUP_ID, "⚔️ New server is live — join now!")
end)

TROUBLESHOOTING

Error Reference

ErrorCauseFix
Http requests are not enabledStudio setting offGame Settings → Security → Allow HTTP Requests → ON
401 UnauthorizedWrong or missing API keyCheck the authorization header in RankConfig matches exactly
Invalid Roblox cookieCookie expired or wrongRe-extract .ROBLOSECURITY from browser — log in as bot first
ClerkJS failed to loadWrong publishable key in HTMLCheck data-clerk-publishable-key in index.html
401 on /portal/registerClerk token missingMake sure x-rp-token is sent with Clerk.session.getToken()
CLERK_SECRET_KEY not setMissing env var on serverAdd to Railway Variables or your .env file
Allowed origin errorDomain not added in ClerkClerk Dashboard → Configure → Allowed Origins → add your domain
User is pending a responseDuplicate call same userIdNormal — wait for previous request to complete before calling again
Number of requests exceededRoblox rate limit hitModule auto-retries after 30s. Reduce call frequency.
Cannot rank above bot's rankTarget rank ≥ bot rankRaise the bot's group rank or target a lower rank number
Server not respondingFree tier cold startFirst request after inactivity takes ~10s. Upgrade for production.
FAQ

Frequently Asked Questions

Why does my cookie expire?

Roblox cookies expire when you log out of the bot account, change its password, or after extended inactivity. When it expires, log back into the portal and submit the new cookie — your API key stays the same, only the cookie behind it updates.

Can I use one API key for multiple groups?

Yes. Every ranking method takes a groupId parameter. As long as the bot account is in each group with the required permissions, you can manage multiple groups with a single key.

Can multiple game servers use the same key simultaneously?

Yes. There's no per-server limit. All your game servers can call the API concurrently — only Roblox's own rate limits on the bot account apply.

How do I find my Group ID?

Go to your group page on Roblox. The URL will be roblox.com/groups/XXXXXXXX/Name — the number is your group ID.

My game works in Studio but not in the live server

The module auto-selects /api in Studio and /roblox live. Make sure your deployed server mounts both paths (default RankPortal does). Also double-check HTTP Requests is enabled — this setting is per-game and sometimes resets on republish.

How many users can sign up on the portal?

Clerk's free tier supports 10,000 monthly active users. For most group ranking portals this is more than enough. You can see all users in the Clerk Dashboard → Users.

Where are API keys and cookies stored?

In data/keys.json on the server. For production, replace this with a real database (PostgreSQL, MongoDB) and encrypt cookie values at rest.