quizbase
Skip to content
Command Palette
Search for a command to run...
QuizBase · Docs
by Maciej Dzierżek · published May 15, 2026 · 40 min read · Beginner
roblox-classroom-experience hero illustration
Illustration for: Roblox classroom trivia experience with HttpService · Generated with Nano Banana, brand style

Roblox classroom trivia experience with HttpService#

Roblox is the platform where 100+ million daily players already are — most of them between 8 and 16, exactly the demographic where teachers struggle to land “engaging classroom content”. This guide builds a Roblox Studio experience that runs as a virtual classroom: a teacher avatar (or moderator-only seat) picks a topic, players stand on numbered platforms (A/B/C/D), question appears on a 3D screen, fastest-correct-team scores.

Roblox HttpService makes real HTTP requests from a server Script. We use it to call QuizBase REST and stream questions into the experience without ever leaving Roblox Studio. The lesson plan is the QuizBase content; the engagement is the Roblox loop.

What you’ll build#

A Roblox experience set in a small classroom with:

  • Four answer platforms (A/B/C/D) where students stand to vote
  • 3D screen at the front displaying the current question text + four shuffled answer choices
  • Teacher seat (only the experience owner can sit there) — clicks “Next question” to advance
  • Server-side scoring — the platform a player stood on at reveal-time = their answer; correct platform’s standers each get +1
  • Topic picker — teacher rotates between QuizBase categories (animals, science, geography, history) via in-game buttons
  • Per-experience question dedup — server tracks question IDs in seenIds: Set so a 45-minute class never sees the same question twice

The mechanic — server-side stable ID dedup#

Roblox server Scripts have a persistent memory for the lifetime of the experience instance. Track seenIds: Set in the server’s Lua state, query /api/v1/questions/random, refetch on collision. Same pattern as the Slack/Discord trivia bot and MCP Slack trivia bot — see /docs/api/questions-by-id for the canonical reference.

Stack#

  • Roblox Studio (free download, Mac + Windows)
  • HttpService — built-in, allows outbound HTTP to allow-listed domains
  • StarterGui + SurfaceGui — text on 3D screen
  • Players + Workspace — touching the answer platforms
  • QuizBase publishable keyqb_pk_* free tier. Set in Roblox Studio’s Game Settings → Secrets (Roblox’s encrypted vault) so the key never appears in your .rbxl file or your published experience.

Step 1 — Set up Roblox Studio + enable HTTP#

In Roblox Studio: File → New → Baseplate. Then Home → Game Settings → Security:

  • Enable ”Allow HTTP Requests” (off by default for new experiences)
  • Add https://quizbase.runriva.com to the HTTP Request Allow List (paste the exact base URL)

Then Game Settings → SecretsNew Secret → name: QUIZBASE_KEY, value: your qb_pk_* key. The secret is never serialised into the saved .rbxl and never exposed to the client LocalScripts — only server Scripts can read it via HttpService:GetSecret().

Step 2 — Wire QuizBase fetch from a server Script#

In ServerScriptService, add a new Script (not LocalScript):

-- ServerScriptService/QuizBaseFetch.lua
local HttpService = game:GetService('HttpService')

local seenIds = {}

local function fetchUnique(category, retries)
	retries = retries or 3
	local secret = HttpService:GetSecret('QUIZBASE_KEY')
	local url = 'https://quizbase.runriva.com/api/v1/questions/random'
	if category then url = url .. '?category=' .. category end

	for attempt = 1, retries do
		local response = HttpService:RequestAsync({
			Url = url,
			Method = 'GET',
			Headers = {
				['X-API-Key'] = secret:AddPrefix(''):AddSuffix(''),
				['Accept'] = 'application/json'
			}
		})

		if response.Success then
			local body = HttpService:JSONDecode(response.Body)
			-- Roblox `Secret` type unwraps via `:AddPrefix` (workaround for legacy
			-- Header API that wants strings — actual modern API: see HttpService
			-- secret-aware Headers param documented in Roblox dev hub).
			if not seenIds[body.id] then
				seenIds[body.id] = true
				return body
			end
		end
	end
	error('No fresh question after ' .. retries .. ' retries')
end

return { fetchUnique = fetchUnique }

The seenIds table is server-process-local. When the experience server restarts (Roblox cycles servers on low player count or after long uptime), the table empties and we start fresh — acceptable for a classroom session that runs for one period.

Step 3 — Render question on the 3D screen#

In Workspace, add a Part shaped like a screen (BlockMesh, 16:9). Inside the part add a SurfaceGui with a TextLabel for the question and four TextLabels for choices. The server Script updates the GUI via :WaitForChild():

-- ServerScriptService/Round.lua
local quiz = require(game.ServerScriptService.QuizBaseFetch)
local screen = workspace:WaitForChild('ScreenPart')
local gui = screen:WaitForChild('SurfaceGui')

local function shuffle(t)
	for i = #t, 2, -1 do
		local j = math.random(i)
		t[i], t[j] = t[j], t[i]
	end
end

local currentRound

local function nextQuestion(category)
	local q = quiz.fetchUnique(category)
	local choices = { q.correctAnswer, table.unpack(q.incorrectAnswers) }
	shuffle(choices)
	local correctIndex
	for i, c in ipairs(choices) do
		if c == q.correctAnswer then correctIndex = i end
	end

	gui.QuestionLabel.Text = q.text
	for i = 1, 4 do
		gui['Choice' .. i].Text = string.char(64 + i) .. '. ' .. choices[i]
	end
	gui.AttributionLabel.Text = 'Source: ' .. q.attribution.author .. ' (' .. q.attribution.license .. ')'

	currentRound = { id = q.id, correctIndex = correctIndex }
end

The AttributionLabel is required — QuizBase content ships under CC BY-SA / CC BY / MIT, and surfacing attribution to the audience is a license requirement. See /data for the full attribution rules. If you skip this you’re out of compliance.

Step 4 — Score the platforms (server-side)#

Add four Parts in Workspace named PlatformA, PlatformB, PlatformC, PlatformD. When a teacher clicks “Reveal”, the server checks which players are touching the correct platform and increments their score:

local Players = game:GetService('Players')
local scores = {}

local function reveal()
	if not currentRound then return end
	local correctPlatform = workspace:WaitForChild('Platform' .. string.char(64 + currentRound.correctIndex))
	for _, player in ipairs(Players:GetPlayers()) do
		local character = player.Character
		if character and character.PrimaryPart then
			local pos = character.PrimaryPart.Position
			local cp = correctPlatform.Position
			local platSize = correctPlatform.Size
			if math.abs(pos.X - cp.X) < platSize.X / 2 and math.abs(pos.Z - cp.Z) < platSize.Z / 2 then
				scores[player.UserId] = (scores[player.UserId] or 0) + 1
			end
		end
	end
	-- Flash the correct platform green for 2 seconds
	correctPlatform.Color = Color3.fromRGB(0, 255, 0)
	wait(2)
	correctPlatform.Color = Color3.fromRGB(200, 200, 200)
end

Step 5 — Teacher controls + category picker#

Add two ClickDetector-equipped parts: “Next question” and “Reveal”. The ClickDetector server event verifies the player is the experience owner (or has a designated TeacherRole attribute):

local owner = game.CreatorId

workspace.NextButton.ClickDetector.MouseClick:Connect(function(player)
	if player.UserId == owner then
		nextQuestion(currentCategory or 'science-and-nature')
	end
end)

workspace.RevealButton.ClickDetector.MouseClick:Connect(function(player)
	if player.UserId == owner then
		reveal()
	end
end)

For category rotation, place four “category” parts (Animals/Science/Geography/History) the teacher steps on between rounds; a Touched event sets currentCategory. Slug values come from /api/v1/categories.

Pitfalls#

  • HttpService:GetSecret returns a Secret userdata, not a string. The legacy HTTP API wants strings in Headers — use the new secret-aware Headers field or :AddPrefix('Bearer ') for Bearer auth. Check the Roblox dev hub for the current API shape; this changes occasionally.
  • HTTP Request Allow List is exact matchhttps://quizbase.runriva.com is allowed, but https://www.quizbase.runriva.com is not. The right URL is the base URL from /docs/api/me docs.
  • Roblox throttles HttpService to ~500 req/min per server, well above free-tier QuizBase’s 10 req/sec burst. You won’t hit either limit for a classroom of 30 players × 1 round per minute.
  • seenIds evaporates on server restart. For a multi-period day, persist via DataStoreService keyed by experience instance. For a single-period session, the in-memory table is enough.

What next#

  • Per-class persistenceDataStoreService saves scores under teacher.UserId so weekly leaderboards work across sessions
  • Multi-language for ESL classrooms — pass ?lang=es to QuizBase, render question in Spanish. See languages and translations
  • Topic deep-dive — replace category-based filter with GET /api/v1/topics/{slug} for “Napoleon trivia” or “biology midterm prep”
  • Submit to Roblox Education — the platform actively rewards classroom-aligned content with Discover-page promotion

Ready to ship? Grab a free publishable key (no card), enable HttpService in Game Settings, paste the snippets in Studio, test in single-player mode. Bug in a question? Roblox can hit POST /api/v1/report directly — just HttpService:RequestAsync with the report payload.