Skip to content

Lua Learning — Networking Layer

This document maps the entire client/server networking surface of Lua Learning: every RemoteEvent and RemoteFunction, the data that flows through it, who fires it, who receives it, and why.

Companion doc: the overall source layout (features vs. kernel/foundation, aliases, the build pipeline) is in architecture.md. This doc assumes that layout.

Scope: Roblox/Luau project. All remotes are wrapped by the vorlias/net library (Net, v3.0.6). Remotes are declared per-feature and aggregated centrally; handlers and callers live across the feature slices (src/features/<X>/{server,client}), the server kernel (src/kernel), the context-split data modules (src/shared/Data), and the client app shell / shared components (src/client/{app,ui}).

Module references below use aliases and omit line numbers (which drift on every edit). To find a remote's exact call sites, grep its name — Remotes.Server:Create("Name") / Remotes.Client:Get("Name").


1. Architecture overview

1.1 Per-feature declaration, central aggregation

There is no single Remotes file anymore. Each feature declares the remotes it owns in its own shared/remotes.luau, returning a plain { name = Net.Definitions.<Kind>() } table:

-- src/features/Quests/shared/remotes.luau
local Net = require("@pkg/Net")
return {
    GetQuestContent = Net.Definitions.ServerAsyncFunction(),
    UpdateQuestMetadatas = Net.Definitions.ServerToClientEvent(),
    -- ...
}

@shared/Remotes/Core holds the app-wide remotes that belong to no single feature (app info, content, economy, notifications, bookmarks). @shared/Remotes then merges the Core group plus every feature's shared/remotes into the single flat table that Net.Definitions.Create expects:

local definitionGroups = {
    require("@shared/Remotes/Core"),
    require("@features/Analytics/shared/remotes"),
    require("@features/Classroom/shared/remotes"),
    -- ...one per feature...
}
-- merged into one table; errors on a duplicate remote name
local Remotes = Net.Definitions.Create(declarations)
return Remotes

It errors on a duplicate remote name across groups, so two features can't silently claim the same remote. Consumers are unchanged: server side Remotes.Server:Create(name), client side Remotes.Client:Get(name).

When the game is not running (Fusion "story"/preview mode), @shared/Remotes short-circuits and returns a mock with no-op Client:Get / Server:Create so UI stories render in isolation without a live server.

1.2 The three remote kinds in use

Although vorlias/net supports more, this codebase uses exactly three definition kinds:

Definition kind Underlying instance Direction Server API Client API
ServerAsyncFunction() RemoteFunction Client → Server → (response back) :SetCallback(fn(player, ...)) returns value :CallServerAsync(...) returns a Promise
ServerToClientEvent() RemoteEvent Server → Client(s) :SendToPlayer(p, ...), :SendToAllPlayers(...), :SendToPlayers({p}, ...), :SendToAllPlayersExcept({p}, ...) :Connect(fn(...))
ClientToServerEvent() RemoteEvent Client → Server (fire-and-forget) :Connect(fn(player, ...)) :SendToServer(...)

Key behavioral notes: - ServerAsyncFunction is the workhorse. Most remotes are request/response RPCs. The client gets a Promise from :CallServerAsync() and uses :andThen() / :catch() / :await(). The server callback's return value becomes the resolved value. Callbacks frequently return a { success = boolean, msg = string?, ... } envelope. - Events carry no acknowledgement. ClientToServerEvent and ServerToClientEvent are one-way; reliability/ordering is Roblox's default RemoteEvent behavior. - Server handles are obtained with Remotes.Server:Create(name); client handles with Remotes.Client:Get(name).

1.3 EncryptedNet — end-to-end encrypted remotes

A small subset of security-sensitive remotes are wrapped with EncryptedNet (@pkg/EncryptedNet, boatbomber/encryptednet@1.0.4). It performs an ECC (elliptic-curve) Diffie-Hellman handshake over a Handshake RemoteFunction at require time, then transparently encrypts (ChaCha20) and signs every payload. The wrapper exposes the same :SendToServer / :Connect / :CallServerAsync / :SetCallback API, so call sites look identical — only the wrapping differs.

local EncryptedNet = require("@pkg/EncryptedNet")
local AttemptLogin = EncryptedNet(Remotes.Server:Create("AttemptLogin")) -- server
local AttemptLogin = EncryptedNet(Remotes.Client:Get("AttemptLogin"))    -- client

The encrypted (credential-bearing) remotes are: AttemptLogin, AttemptRegister (Moderation), AttemptClassroomLogin, RegisterClassroomPassword (Classroom).

1.4 Idiomatic patterns

Request/response (ServerAsyncFunction):

-- server (in a feature's server slice)
local GetProfile = Remotes.Server:Create("GetProfile")
GetProfile:SetCallback(function(Player, userId) return profileFor(userId) end)
-- client
Remotes.Client:Get("GetProfile"):CallServerAsync(userId):andThen(function(profile) ... end)

Server broadcast (ServerToClientEvent):

local ReceiveCoins = Remotes.Server:Create("ReceiveCoins")   -- server
ReceiveCoins:SendToPlayer(Player, amount)
Remotes.Client:Get("ReceiveCoins"):Connect(function(amount) ... end)  -- client

Client signal (ClientToServerEvent):

Remotes.Client:Get("HaltCode"):SendToServer()               -- client
Remotes.Server:Create("HaltCode"):Connect(function(Player) ... end)  -- server

1.5 Directory map of networking code

src/shared/Remotes/
  init.luau            ← aggregates Core + every feature's remotes into one Net.Definitions.Create
  Core.luau            ← app-wide remotes (app info, content, economy, notifications, bookmarks)
src/features/<X>/shared/remotes.luau   ← each feature's own remote declarations
src/features/<X>/server/               ← :Create + :SetCallback / :Connect / :Send*
src/features/<X>/client/               ← :Get + :CallServerAsync / :Connect / :SendToServer
src/kernel/                            ← PlayerData, FFlags, MessagesHandler (Core remote handlers)
src/shared/Data/{Content,Gamepasses}/  ← context-split data modules that own a few remotes
src/client/{app,ui}/                   ← app-shell + shared-component callers
Packages/Net                           ← vorlias/net@3.0.6
Packages/EncryptedNet                  ← boatbomber/encryptednet@1.0.4 (ECC-encrypted wrapper)

1.6 Remote count by owning module

Group Owner Count
Core (app info, content, economy, notifications, bookmarks) @shared/Remotes/Core 15
Analytics @features/Analytics 2
Lessons @features/Lessons 2
Tutorials (images, tutorials, comments, submissions) @features/Tutorials 22
Quests @features/Quests 9
Code Running (shared, quest, lesson) @features/CodeRunning 15
AI Tutor @features/TutorAI 6
Profile @features/Profile 1
Settings @features/Settings 2
Onboarding @features/Onboarding 2
Monetization @features/Monetization 6
Feedback @features/Feedback 2
IDE @features/IDE 1
Deeplinking @features/Deeplinking 1
Moderation @features/Moderation 10
Classroom (security, discussions, comments, assignments) @features/Classroom 22
Total 118

Some remotes are declared in Core but handled outside the kernel, and a few feature remotes are handled in shared data modules — the count above is by declaring module. Handler locations are given per-remote below.


2. Core — app-wide

Declared in @shared/Remotes/Core. These belong to no single feature; most are handled by the server kernel (@kernel/PlayerData, @kernel/FFlags, @kernel/MessagesHandler) or by the context-split @shared/Data/Content module.

2.1 App info

GetServerTypeServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: the app loader at startup to decide which app variant to mount — @app/AppLoader.
  • Received by: @kernel/PlayerData.
  • Request payload: none
  • Response: a string — one of:
  • "Private"game.PrivateServerId ~= "" and PrivateServerOwnerId ~= 0 (a school/classroom private server)
  • "Reserved" — reserved server with no owner (temporary lobby used during shutdown cycling)
  • "Standard" — public server
  • Why: AppLoader maps "Reserved" → ShutdownApp, "Private" → Classroom*App, otherwise the normal Desktop/Mobile/Console app. This is the first decision in client bootstrap.

GetFFlagsServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: the shared settings module during settings load — @ui/settings.
  • Received by: @kernel/FFlags.
  • Request payload: none
  • Response: the feature-flag table sourced from a Google Sheet via SheetValues (each flag keyed by name).
  • Why: Pulls the current set of feature flags (betas) on startup so the client can gate experimental features. Stored into settings.betas.

BroadcastFFlagsServerToClientEvent

  • Direction: Server → all Clients
  • Fired by: @kernel/FFlags (:SendToAllPlayers(...), triggered by the sheet's Changed).
  • Received by: @ui/settings.
  • Payload: the full feature-flags table (same shape as GetFFlags' response).
  • Why: Live-updates feature flags to every connected client whenever the backing Google Sheet changes, without requiring a rejoin.

ShutdownWarningServerToClientEvent

  • Direction: Server → all Clients
  • Fired by: @kernel/MessagesHandler (:SendToAllPlayers(message, dismissDelay)), driven by a cross-server MessagingService "Shutdown" message. Counts down minutes then seconds.
  • Received by: the app shells — @app/DesktopApp, @app/MobileApp, @app/ClassroomDesktopApp.
  • Payload: (message: string, dismissDelay: number)
  • Why: Warns players of an imminent server restart/update. Rendered as a Toast that auto-dismisses after dismissDelay seconds.

2.2 Content

The Content system streams the game's lesson/tutorial/quest content (sourced from the LuaLearningRBLX/Content GitHub repo) to clients on demand. Its server/client live in @shared/Data/Content.

RetrieveContentServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @shared/Data/Content/Client — lazily fetches a content key the first time it's accessed.
  • Received by: @shared/Data/Content/Server.
  • Request payload: (key: string?) — a top-level content section name (e.g. "Lessons", "Tutorials"). If nil, the entire content table is returned.
  • Response: Server.Value[key] (or the whole table). The server waits up to 1 second for content to finish loading before responding.
  • Why: Pulls large content blobs only when a section is opened, rather than replicating everything at join.

BroadcastContentServerToClientEvent

  • Direction: Server → all Clients
  • Fired by: @shared/Data/Content/Server:SendToAllPlayers(key) for each content key whose value changed after a re-download (triggered by a MessagingService "ContentRefresh" message).
  • Received by: @shared/Data/Content/Client — re-fetches that key if it was already loaded.
  • Payload: (key: string) — the name of the content section that changed.
  • Why: Live content updates without a rejoin — when the content repo changes, clients refresh just the affected sections.

FilterTextServerAsyncFunction

Not to be confused with the server-only FilterText module (the actual filtering implementation). This remote is the client-facing wrapper around Roblox's text filter. - Direction: Client → Server (RPC) - Fired by: UIs that filter user text before previewing — @features/Quests/client/Desktop/QuestDiscover, the Classroom AssignmentsBrowse/{StudentBrowser,TeacherBrowser} and DiscussionsSection/DiscussionDiscover (under @app/ClassroomDesktopApp). - Received by: @kernel/PlayerData. - Request payload: (Text: string | { [any]: string }) — a single string or a map of strings to filter in one round-trip. - Response: the filtered string, or a same-keyed table of filtered strings (or "Filter Error" if the input was neither). Filtering is scoped to Player.UserId. - Why: Lets client UIs display Roblox-filtered versions of user-entered text (search queries, previews) consistent with Roblox moderation rules.

2.3 Economy

Currency/reputation values pushed to the client whenever the server's backing values change. Both are driven by GlobalStorage key-changed signals in @kernel/PlayerData.

ReceiveCoinsServerToClientEvent

  • Direction: Server → Client
  • Fired by: @kernel/PlayerData (on Coins key change, plus the initial value at join, default 100).
  • Received by: @app/DesktopApp, @app/MobileAppsharedState.coins:set(amount).
  • Payload: (amount: number)
  • Why: Keeps the client's coin balance in sync with the server-authoritative value.

ReceiveReputationServerToClientEvent

  • Direction: Server → Client
  • Fired by: @kernel/PlayerData (on Reputation key change, plus the initial value at join, default 0).
  • Received by: @app/DesktopApp, @app/MobileAppsharedState.reputation:set(amount).
  • Payload: (amount: number)
  • Why: Keeps the client's reputation total in sync with the server-authoritative value.

2.4 Notifications

In-app notification inbox. Notifications are stored as a map keyed by id in the player's GlobalStorage "Notifications" value. All handled by @kernel/PlayerData.

Notifications are classroom-aware: in a classroom server, RequestNotifications reads from a <classroomId>/<userId> store; otherwise from the global PlayerGlobal<userId> store.

RequestNotificationsClientToServerEvent

  • Direction: Client → Server (fire-and-forget)
  • Fired by: the notification popups — @app/{DesktopApp,MobileApp,ClassroomDesktopApp}/AppComponents/PopupMenus/Notifications.
  • Received by: @kernel/PlayerData — also lazily wires up a Notifications key-changed connection that pushes future changes via ReceiveNotifications.
  • Payload: none
  • Why: Asks for the current notifications and subscribes the player to live updates.

ReadNotificationClientToServerEvent

  • Direction: Client → Server (fire-and-forget)
  • Fired by: the notification popups, when a notification scrolls into view or is opened.
  • Received by: @kernel/PlayerData — sets notifs[notifId].Unread = false and echoes back via ReceiveNotifications.
  • Payload: (notifId) — the id of a single notification.
  • Why: Marks one notification as read.

ReadAllNotificationsClientToServerEvent

  • Direction: Client → Server (fire-and-forget)
  • Fired by: (no client caller in the current source) — defined and handled, but not currently invoked; kept available for a "mark all read" action.
  • Received by: @kernel/PlayerData — sets Unread = false on every notification, then echoes back via ReceiveNotifications.
  • Payload: none
  • Why: Marks the entire inbox as read in one shot.

ReceiveNotificationsServerToClientEvent

  • Direction: Server → Client
  • Fired by: @kernel/PlayerData at multiple points: on the Notifications key change, on initial join, and after ReadNotification / ReadAllNotifications.
  • Received by: the notification popups.
  • Payload: (notifications: { [id]: { Unread: boolean, ... } }) — the full notifications map (defaults {}).
  • Why: Delivers the player's notification set (initial + live updates + post-read echoes).

2.5 Bookmarks

Per-player cross-content bookmarks, backed by the "bookmarks" DataStore2 store and handled by @kernel/PlayerData.

GetBookmarksServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: the app shells at load — @app/DesktopApp, @app/MobileApp → sets sharedState.bookmarks.
  • Received by: @kernel/PlayerData.
  • Request payload: none
  • Response: the player's bookmarks table (defaults {}).
  • Why: Loads saved bookmarks on startup.

SetBookmarksServerAsyncFunction

A ServerAsyncFunction despite the "Set" name — the client awaits a success/failure envelope. - Direction: Client → Server (RPC) - Fired by: the app shells, whenever sharedState.bookmarks changes (wired up only after the initial GetBookmarks load) — @app/DesktopApp, @app/MobileApp. - Received by: @kernel/PlayerData. - Request payload: (newBookmarks: table) — rejected if not a table. - Response: { success, msg } (success = false, msg = "Invalid bookmarks" on bad input; otherwise success = true, msg = "Bookmarks saved"). - Why: Persists bookmark changes.


3. Analytics — @features/Analytics

Telemetry channels: one reports how long a player dwells in each section; the other mirrors server-side errors to clients for live diagnostics.

SectionTimeAnalyticsClientToServerEvent

  • Direction: Client → Server (fire-and-forget)
  • Fired by: @features/Analytics/client/SectionTracking whenever the open section changes.
  • Received by (two independent handlers):
  • @features/Analytics/server/SectionTracking — commits the time spent in the previous section and starts a new timer.
  • @features/TutorAI/server/Util — records the player's current section into the AI tutor's player-context.
  • Payload: (sectionName: string) — validated server-side against an allow-list (Tutorials, Lessons, Quests, TutorAI, Multiplayer, Other, Discussions, Assignments, Gradebook, Dashboard).
  • Why: Drives GameAnalytics SectionTracking:TimeSpent:<section> design events (only if ≥1 minute, not in Studio) and keeps the AI tutor aware of the player's location.

ShowServerErrorServerToClientEvent

  • Direction: Server → Client(s)
  • Fired by: @features/Analytics/server/ErrorSharing:SendToAllPlayers(...) on every new server warning/error (LogService.MessageOut), and :SendToPlayer(...) to replay log history on join.
  • Received by: @features/Analytics/client/ErrorSharing — surfaces into the in-app developer console/output.
  • Payload: (formattedMessage: string) — pre-formatted as "[server err|info|output|warn]: <message>". Only warnings/errors are shared, and never in Studio.
  • Why: Mirrors server-side errors into the client's output so creators/players can see server faults that would otherwise be invisible client-side.

4. Lessons — @features/Lessons

Per-player lesson position tracking. Handled by @features/Lessons/server/init, backed by the "state" DataStore2 store. (Lesson code execution is the CodeRunning feature, §7.)

ViewLessonClientToServerEvent

  • Direction: Client → Server (fire-and-forget)
  • Fired by: @features/Lessons/client/Desktop when a lesson is opened.
  • Received by (two handlers):
  • @features/Lessons/server/init — persists state.lastLesson = { chapter, lesson, timestamp }.
  • @features/TutorAI/server/Util — caches the current lesson's metadata into the AI tutor's player context.
  • Payload: (chapter: number, lesson: number)
  • Why: Records the user's most recent lesson (for "resume where you left off") and informs the AI tutor of the lesson the player is viewing.

GetLastLessonServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @app/DesktopApp at load → sets sharedState.lastLesson.
  • Received by: @features/Lessons/server/init.
  • Request payload: none
  • Response: { chapter, lesson, timestamp } from the "state" store (defaults { 1, 1, 0 }).
  • Why: Lets the client restore the "continue learning" pointer on startup.

5. Tutorials — @features/Tutorials

Community tutorials: image helpers, chunked metadata browsing, full content fetch, voting, coin-funded "awards", a threaded comment system, and the author-side submission/draft lifecycle. Server logic spans @features/Tutorials/server/{init,Comments,Submissions}. Most responses use a { success, data | msg } envelope.

AI-tutor side channel: ServerAsyncFunctions only allow one callback, so the AI tutor "hacks in" an extra OnServerEvent listener on the underlying remote instances of GetTutorialContent and GetQuestContent (in @features/TutorAI/server/Util) to learn what the player is reading. (This works because the net library implements async functions over RemoteEvents.)

5.1 Images — @features/Tutorials/server/init

GetImageSizeServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: markdown image rendering and image inputs — @ui/Components/Markdown, @app/ClassroomDesktopApp/AppComponents/AssignmentsSection/AssignmentsEditor/InputComponent/Image.
  • Received by: @features/Tutorials/server/init (server-cached).
  • Request payload: (id: string) — an asset id (digits extracted via string.match).
  • Response: success { success = true, data = { width, height, ... } } (queried from api.boatbomber.com/roblox/image-size); failure { success = false, msg }.
  • Why: Lets the client compute correct aspect ratio for embedded images before they load (avoiding layout jumps). Cached server-side.

GetImageFromDecalServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @ui/Components/Markdown/getImageId — when a referenced asset is a Decal (AssetTypeId 13).
  • Received by: @features/Tutorials/server/init (server-cached).
  • Request payload: (id: string) — a decal asset id.
  • Response: success { success = true, data = { imageId } } — resolved via InsertService:LoadAsset, reading the Decal's Texture; failure { success = false, msg }.
  • Why: Markdown image references may point at a Decal rather than the raw Image; this resolves the real rbxassetid://.

5.2 Tutorials — @features/Tutorials/server/init

GetTutorialMetadatasServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @shared/Data/Content/Client (loadTutorialChunk), called repeatedly with increasing chunk until endChunks.
  • Received by: @features/Tutorials/server/init.
  • Request payload: (chunk: number) — chunk index into the tutorial store.
  • Response: end of data { success = false, msg = "End of chunks" }; success { success = true, data = { [key] = { Key, Title, Desc, AuthorId, Date, Votes, Awards } } } — slimmed metadata only (no content body).
  • Why: Lets clients page through tutorial metadata for the browse view without downloading full bodies.

GetTutorialContentServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @features/Tutorials/client/{Desktop,Mobile}/TutorialReader.
  • Received by: @features/Tutorials/server/init (plus the AI-tutor side-channel in @features/TutorAI/server/Util).
  • Request payload: (key: string)
  • Response: { success = true, data = <tutorial.Content> } or { success = false, msg = "Tutorial not found" }. Side effect: marks the tutorial read in the player's "stats" store and awards badge 2124484900 once 75 tutorials are read.
  • Why: Fetches the full tutorial body on open, and tracks reading progress / read-count badges.

GetPersonalTutorialVoteServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @features/Tutorials/client/{Desktop,Mobile}/TutorialReader.
  • Received by: @features/Tutorials/server/init.
  • Request payload: (key: string)
  • Response: { success = true, data = <vote> } where vote ∈ {-1, 0, 1} from the "votes" store.
  • Why: Shows the user their current vote state on a tutorial.

SetPersonalTutorialVoteClientToServerEvent

  • Direction: Client → Server (fire-and-forget)
  • Fired by: @features/Tutorials/client/{Desktop,Mobile}/TutorialReader.
  • Received by: @features/Tutorials/server/init — pushes (key, value) into a per-player TutorialVoteQueue, flushed in batches.
  • Payload: (key: string, value: number) — the new vote (-1 | 0 | 1).
  • Why: Records the user's up/down vote; queued/batched to avoid hammering the vote store.

GiveTutorialAwardServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @features/Tutorials/client/{Desktop,Mobile}/TutorialReader/AwardPanel.
  • Received by: @features/Tutorials/server/init.
  • Request payload: (tutorialId: string, awardName: string)
  • Response: { success, data }. Failure data: "Invalid award", "Not enough coins", "Tutorial not found", "Can't give yourself an award". On success, deducts the award's coin Price and increments tutorial.Awards[awardName].
  • Why: Lets a reader spend coins to grant a tutorial author an award (coin sink + author reward).
  • Direction: Server → Client
  • Fired by: @features/TutorAI/server/Tools — an AI-tutor "tool" that opens a tutorial for the player.
  • Received by: @features/Tutorials/client/{Desktop,Mobile} — opens the named tutorial in the reader.
  • Payload: (tutorialId: string)
  • Why: Allows the AI tutor to navigate the user directly to a relevant tutorial.

5.3 Tutorial Comments — @features/Tutorials/server/Comments

A threaded comment system on tutorials, stored as an MPTT (modified-preorder tree-traversal) tree per tutorial. All client usage is in the Desktop/Mobile TutorialReader.

Trees are stored oldest-first, so posting a comment appends at the end of its sibling group and no write ever has to reorder the tree. The reader's chosen order is applied when a window is packed, so a response only ever carries the slice being displayed, in the order it displays in. The orders on offer live in @features/Tutorials/shared/CommentSorts, which both sides share so the client can place a merged page exactly where the server would have put it. Best is the considered one, ranking by the Wilson lower bound on a comment's votes so a couple of upvotes counts as weak evidence, plus a bounded award bonus that saturates rather than letting awards run away with the ranking. The other three are plain numeric orders, a straight vote tally for Top and posting time for Newest and Oldest. Anything unrecognized falls back to the default, Best.

Two-phase loading: the fetch remotes return a windowed tree skeleton with Content, AuthorId, and individual awards stripped, carrying only what ordering and rendering need. The client then calls PopulateTutorialComment per node to fetch the filtered content lazily.

GetTutorialCommentsServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: @features/Tutorials/client/{Desktop,Mobile}/TutorialReader@features/Tutorials/server/Comments.
  • Request payload: (key: string, sortName: string) — one of CommentSorts.List.
  • Response: { success = true, data = <packed window> } — the first five top-level comments in the requested order with one reply previewed under each, plus the requester's own comments and the ancestors that connect them. The payload is parallel arrays of Ids, Depths, CreationTimes, TotalVotes, Upvotes, sparse AwardTotals and Deleted maps, and ChildCounts telling the client how much more sits under each node. Content, author, and which awards a comment holds are populated on demand.
  • Why: Loads just the comments the reader can see, ordered the way they asked for.

GetMoreTutorialCommentsServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: TutorialReader@features/Tutorials/server/Comments.
  • Request payload: (key: string, parentId: string, haveIds: {string}, count: number, sortName: string)haveIds is the set of that parent's children the client already holds, which keeps paging gapless even while votes reshuffle the ordering between requests. count is clamped to 12 and haveIds to 100 entries.
  • Response: { success = true, data = <packed window> } in the same shape as GetTutorialComments, or { success = false, data = "Comment not found" } for an unknown parent. Slices under the root preview one reply per comment, while deeper slices carry no previews since those replies stay collapsed until expanded.
  • Why: Pages the next batch of comments or replies in without resending what's already loaded.

PopulateTutorialCommentServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: TutorialReader@features/Tutorials/server/Comments.
  • Request payload: (key: string, id: string)
  • Response: { Content, AuthorId, CreationTime, Deleted, Awards, PersonalVote } (or nil). Content is Roblox-text-filtered for the requester; PersonalVote is the requester's own vote.
  • Why: Fetches the full, filtered details for a single comment when it scrolls into view.

SetTutorialCommentVoteClientToServerEvent

  • Direction: Client → Server (fire-and-forget)
  • Fired by / Received by: TutorialReader@features/Tutorials/server/Comments — enqueues into a per-player CommentVoteQueue (batched).
  • Payload: (key: string, id: string, value: number) — tutorial key, comment id, vote (-1 | 0 | 1).
  • Why: Records a vote on an individual comment; batched to limit store writes.

GiveCommentAwardServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @features/Tutorials/client/{Desktop,Mobile}/TutorialReader/AwardPanel.
  • Received by: @features/Tutorials/server/Comments.
  • Request payload: (key: string, id: string, awardName: string)
  • Response: { success, data }. Failure data: "Invalid award", "Not enough coins", "Cannot award that user". On success, deducts the award price and increments node.Awards[awardName]. The stored order doesn't change, so the new award only moves the comment the next time a window is packed under a sort that counts awards.
  • Why: Lets readers spend coins to award a specific comment's author.

PostTutorialCommentServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: TutorialReader@features/Tutorials/server/Comments.
  • Request payload: (key: string, id: string, content: string)id is the parent node ("_ROOT" for a top-level comment).
  • Validation / failure data: "Invalid content", "Content too short" (<20), "Content too long" (>600), email/Discord-handle guards, "Content was rejected by the filter" (Damerau- Levenshtein vs filtered >25%), "Tutorial not found", "Couldn't find target node".
  • Response: { success = true, data = "Posted successfully!", id = <newId> }. Also notifies the parent node's author with a ClickInfo deep-link { Type = "TutorialLink", TutorialId, CommentId }.
  • Why: Adds a new comment/reply to a tutorial's comment tree and notifies the author.

EditTutorialCommentServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: TutorialReader@features/Tutorials/server/Comments.
  • Request payload: (key: string, id: string, content: string)
  • Validation: identical content checks to PostTutorialComment. Only the node's own author may edit.
  • Response: { success = true, data = "Edited successfully!" } (failures mirror Post).
  • Why: Lets an author edit the text of their own comment.

DeleteTutorialCommentServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: TutorialReader@features/Tutorials/server/Comments.
  • Request payload: (key: string, id: string)
  • Response: { success, data }. data: "Comment not found", "Already deleted", "You do not have permission to delete this comment" (must be the author or hold Permissions.Comments), or "Deleted successfully!".
  • Why: Removes a comment from the tree (author self-delete or moderator delete).

5.4 Submissions (tutorial drafts) — @features/Tutorials/server/Submissions

"Submissions" are user-authored tutorial drafts moving through the moderation pipeline (draft → submitted/pending → accepted → published; the moderator side is §16). Each user's drafts live in an InfStore keyed by UserId. All client usage is in @features/Tutorials/client/Desktop/TutorialCreate.

GetSubmissionMetadatasServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: TutorialCreate@features/Tutorials/server/Submissions.
  • Request payload: none
  • Response: { success = true, data = { [key] = { Key, Title, Desc, Date, Status } } } — slimmed metadata of the caller's own submissions.
  • Why: Lists the author's drafts/submissions in the tutorial creator.

GetSubmissionContentServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: TutorialCreate@features/Tutorials/server/Submissions.
  • Request payload: (key: string)
  • Response: { success = true, data = <submission.Content> } or { success = false, msg = "Tutorial not found" }.
  • Why: Loads the full draft body for editing.

CreateSubmissionServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: TutorialCreate@features/Tutorials/server/Submissions.
  • Request payload: none
  • Response: { success = true, data = <key> }. Adds a blank "Untitled" draft (Status = "Unsubmitted Draft").
  • Why: Starts a new tutorial draft.

SaveSubmissionServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: TutorialCreate@features/Tutorials/server/Submissions.
  • Request payload: (key: string, data: { Content, Title, Desc }) — each field type-validated.
  • Response: { success, msg?, validatedSubmission? }. Skips writing if unchanged. Editing resets a draft's status to "Unsubmitted Draft"; re-queues a Pending item for re-audit.
  • Why: Persists edits to a draft and keeps the moderation queue consistent.

SubmitSubmissionServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: TutorialCreate@features/Tutorials/server/Submissions.
  • Request payload: (key: string)
  • Response: { success, msg, ... }. Validation: content ≥50, title 5–40, description 10–140. Costs 25 coins for a brand-new submission (edits to an already-published tutorial are free).
  • Why: Submits a draft into the moderation queue (coin-gated for new tutorials).

UnsubmitSubmissionServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: TutorialCreate@features/Tutorials/server/Submissions.
  • Request payload: (key: string)
  • Response: { success, msg, validatedSubmission? }. Resets status and removes the item from both moderation queues.
  • Why: Lets an author pull a submission back out of the moderation queue.

DeleteSubmissionServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: TutorialCreate@features/Tutorials/server/Submissions.
  • Request payload: (key: string)
  • Response: { success, msg } ("Tutorial not found" or "Deleted successfully"). Removes the draft from the user store plus both moderation queues.
  • Why: Deletes a draft submission entirely.

6. Quests — @features/Quests

Covers both playing quests (a grid/board puzzle) and authoring them. Browsing/playing is in @features/Quests/server/init; the create/edit/publish lifecycle is in @features/Quests/server/Authoring. Published quests are in questsStore; per-user drafts in a FreedumbStore keyed by UserId. (Quest code execution is the CodeRunning feature, §7.)

Quest locator (QuestLoc) — most quest reads take a locator table: { scope = "official" | "global" | "player", id = number | string, chunkIndex = number? }. official quests come from the GitHub Content (numeric ids); global quests are user-created (string GUID ids, paged by chunkIndex).

GetQuestMetadatasServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @shared/Data/Content/Client — paged with { scope = "global", chunkIndex } until endChunks.
  • Received by: @features/Quests/server/init.
  • Request payload: (QuestLoc) — uses scope + chunkIndex.
  • Response: { success, message, data?, endChunks? }. For global, data is a chunk of per-quest metadata. The official branch is deprecated (Content handles official quests automatically).
  • Why: Pages through user-created quest metadata for the quest browser.

UpdateQuestMetadatasServerToClientEvent

  • Direction: Server → Client
  • Fired by: @features/Quests/server/initOnChunkChanged pushes freshly processed chunk metadata to each player.
  • Received by: @shared/Data/Content/Client — merges the updated chunk into the client's quest metadata.
  • Payload: (processedChunk) — a map of quest metadata.
  • Why: Live-updates quest metadata when the global quests store changes, without a re-fetch.

GetQuestContentServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @features/Quests/client/Desktop.
  • Received by: @features/Quests/server/init (plus the AI-tutor side-channel in @features/TutorAI/server/Util).
  • Request payload: (QuestLoc)scope + id.
  • Response: { success, message, data? } where data is the full quest object (board, goals, instructions).
  • Why: Loads a full quest to play it.

ReportQuestServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: @features/Quests/client/Desktop@features/Quests/server/init.
  • Request payload: (QuestId: string) — a global quest GUID.
  • Response: { success, message }. Records Quest.Reports[<reporterUserId>] = true.
  • Why: Lets players flag a user-created quest for moderation.

GetUserQuestsServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @features/Quests/client/Desktop/QuestCreate.
  • Received by: @features/Quests/server/init.
  • Request payload: (userId?) — defaults to the caller.
  • Response: { success, message, data? } — all of that user's quests from their FreedumbStore.
  • Why: Lists a user's own authored quests in the quest creator.

CreateQuestServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @features/Quests/client/Desktop/QuestCreate.
  • Received by: @features/Quests/server/Authoring.
  • Request payload: none
  • Response: { success = true, data = <key> }. Creates a blank 5×5 quest draft (Status = "Unsubmitted Draft").
  • Why: Starts a new quest draft and returns its id.

SaveQuestServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @features/Quests/client/Desktop/QuestCreate.
  • Received by: @features/Quests/server/Authoring.
  • Request payload: (QuestId: string, Quest: table)
  • Response: { success, message?, err?, extraData?, validatedSubmission? }. Runs QuestData.Validation.ValidateSubmission; skips writing if unchanged.
  • Why: Persists edits to a quest draft (with validation).

DeleteQuestServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @features/Quests/client/Desktop/QuestCreate.
  • Received by: @features/Quests/server/Authoring.
  • Request payload: (QuestId: string)
  • Response: { success, message }. Removes the quest from both the draft store and questsStore.
  • Why: Deletes a user's quest (draft + published copies).

PublishQuestServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @features/Quests/client/Desktop/QuestCreate.
  • Received by: @features/Quests/server/Authoring.
  • Request payload: (QuestId: string)
  • Response: { success, message?, err?, extraData? }. Requires: passes validation, a valid playtest signature exists, caller is the author, and ≥25 coins (charges 25 on publish).
  • Why: Publishes a validated, playtested quest to the public board (coin-gated).

7. Code Running — @features/CodeRunning

The heart of the learning experience: the player writes Luau code that the server executes in a sandbox ("Job") with a time limit, streaming output/current-line/status back live, then evaluating success. Two flavors — Lessons (run code, check a condition) and Quests (drive a grid-board puzzle) — plus shared infrastructure. Orchestration is in @features/CodeRunning/server/init; the per-job streaming helpers are in @features/CodeRunning/server/Runner/JobInjections/{Quest,Lesson}.

On an execution Error, the server kicks off an AI-tutor "debug help" conversation (AIDebugHelp), budget-permitting (see §8).

Output Type is a string tag ("Normal", "Special", "Error", …) used by the client console to style each output line.

7.1 Shared

HaltCodeClientToServerEvent

  • Direction: Client → Server (fire-and-forget)
  • Fired by: the stop buttons — @features/Quests/client/Desktop/QuestWorkspace, @features/Lessons/client/Desktop/Workspace/Lesson.
  • Received by: @features/CodeRunning/server/init — halts all of the player's active jobs.
  • Payload: none
  • Why: Lets the user stop a running/looping program.

7.2 Quest code running

Handlers in @features/CodeRunning/server/init; per-action streaming from @features/CodeRunning/server/Runner/JobInjections/Quest. Client side: the quest workspace (@features/Quests/client/Desktop/QuestWorkspace) and quests section (@features/Quests/client/Desktop).

RunQuestCodeClientToServerEvent

  • Direction: Client → Server (fire-and-forget)
  • Fired by: @features/Quests/client/Desktop/QuestWorkspace.
  • Received by: @features/CodeRunning/server/init.
  • Payload: (QuestLoc, Code: string)
  • Behavior: Loads the quest (official from Content / global from QuestsStore / player draft), restores collected-coin state, runs the code as a "Quest" sandbox job (180s limit), and on completion evaluates goals → QuestEvaluationResult. First-time coin collection awards +1 coin; beating a global quest increments its Completions; beating a player draft records a validated signature (enables publishing).
  • Why: Executes the player's quest solution and drives the whole quest play/eval loop.

QuestCodeStatusServerToClientEvent

  • Direction: Server → Client
  • Fired by: @features/CodeRunning/server/inittrue at start, false on completion/error/invalid.
  • Received by: @features/Quests/client/Desktop/QuestWorkspace.
  • Payload: (isRunning: boolean)
  • Why: Toggles the running/stop UI state for quests.

QuestOutputServerToClientEvent

  • Direction: Server → Client
  • Fired by: @features/CodeRunning/server/init (status/halt messages) and the job injection in .../JobInjections/Quest (per-action output).
  • Received by: @features/Quests/client/Desktop/QuestWorkspace.
  • Payload: (Text: string, Type: string)
  • Why: Streams console output lines from the running quest program.

QuestLineServerToClientEvent

  • Direction: Server → Client
  • Fired by: .../JobInjections/Quest during execution; -1 on finish clears the highlight.
  • Received by: @features/Quests/client/Desktop/QuestWorkspace.
  • Payload: (lineNumber: number) — currently executing line (-1 = none).
  • Why: Highlights the line of code currently running in the quest editor.

QuestSyncServerToClientEvent

  • Direction: Server → Client
  • Fired by: quest job injections whenever the board state changes (move/interact/rotate). Created eagerly in @features/CodeRunning/server/init so clients can connect.
  • Received by: @features/Quests/client/Desktop.
  • Payload: (Board) — the full board state (player position/rotation, tiles, floors, metadata, goals-met).
  • Why: Keeps the client's visual board in sync with the authoritative simulation as the code runs.

QuestAnimateServerToClientEvent

  • Direction: Server → Client
  • Fired by: the move injection's Animate callback (.../JobInjections/Quest). Created eagerly in @features/CodeRunning/server/init.
  • Received by: passed as the animateEvent prop into the quest board visual in @features/Quests/client/Desktop/QuestWorkspace.
  • Payload: (id: string, duration: number, dontFocus: boolean?)
  • Why: Triggers a board animation for a duration, optionally without focusing the camera.

QuestGoalMetServerToClientEvent

  • Direction: Server → Client
  • Fired by: Injections.checkGoals when a goal first becomes satisfied (.../JobInjections/Quest).
  • Received by: @features/Quests/client/Desktop/QuestWorkspace (onGoalMet).
  • Payload: (goalIndex: number)
  • Why: Lets the UI tick off each quest goal as it's achieved mid-run.

QuestEvaluationResultServerToClientEvent

  • Direction: Server → Client
  • Fired by: @features/CodeRunning/server/init — once the job finishes and goals are evaluated.
  • Received by: @features/Quests/client/Desktop/QuestWorkspace.
  • Payload: (isSuccess: boolean) — all non-hint goals met.
  • Why: Tells the client whether the quest was solved (success → completion flow).

GetQuestCoinStateServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @features/Quests/client/Desktop.
  • Received by: @features/CodeRunning/server/init.
  • Request payload: (questId)
  • Response: { ["x,y"] = true, ... } — which coin tiles the player has already collected (from the "quest_coins" store; defaults {}).
  • Why: Lets the client render already-collected coins as collected (so they're not re-awarded/re-shown).

7.3 Lesson code running

Handlers in @features/CodeRunning/server/init; per-action streaming from @features/CodeRunning/server/Runner/JobInjections/Lesson. Client side: @features/Lessons/client/Desktop/Workspace/Lesson.

RunLessonCodeClientToServerEvent

  • Direction: Client → Server (fire-and-forget)
  • Fired by: @features/Lessons/client/Desktop/Workspace/Lesson.
  • Received by: @features/CodeRunning/server/init.
  • Payload: (Code: string, ChapterNum: number, LessonNum: number)
  • Behavior: Validates chapter/lesson, runs the code in a "Lesson" sandbox (100s limit), then runs the lesson's Condition(Code, EnvTrack) to evaluate success → LessonEvaluationResult, logging a GameAnalytics progression event (Complete/Fail).
  • Why: Executes the learner's code for a lesson and checks the lesson's pass condition.

LessonCodeStatusServerToClientEvent

  • Direction: Server → Client
  • Fired by: @features/CodeRunning/server/inittrue at start, false on finish/invalid.
  • Received by: @features/Lessons/client/Desktop/Workspace/Lesson.
  • Payload: (isRunning: boolean)
  • Why: Toggles the lesson run/stop UI state.

LessonOutputServerToClientEvent

  • Direction: Server → Client
  • Fired by: @features/CodeRunning/server/init and Injections.LessonOutput (.../JobInjections/Lesson).
  • Received by: @features/Lessons/client/Desktop/Workspace/Lesson.
  • Payload: (Text: string, Type: string)
  • Why: Streams console output from the running lesson program.

LessonLineServerToClientEvent

  • Direction: Server → Client
  • Fired by: Injections.LessonLineRepl (.../JobInjections/Lesson); -1 on finish.
  • Received by: @features/Lessons/client/Desktop/Workspace/Lesson.
  • Payload: (lineNumber: number) — currently executing line (-1 = none).
  • Why: Highlights the active line in the lesson editor.

LessonEvaluationResultServerToClientEvent

  • Direction: Server → Client
  • Fired by: @features/CodeRunning/server/init (and the error fallback).
  • Received by: @features/Lessons/client/Desktop/Workspace/Lesson.
  • Payload: (result: boolean, message: string?)
  • Why: Tells the client whether the lesson passed (with an optional explanation); success triggers the confetti/completion celebration.

8. AI Tutor — @features/TutorAI

The conversational AI tutor. The client sends user inputs (start/message/rate) as ClientToServerEvents; the server streams back the conversation snapshot, individual AI replies, and a typing indicator via ServerToClientEvents. Server logic is in @features/TutorAI/server/{init,ConversationManager}; the UIs are the Desktop/Mobile TutorAI sections (@features/TutorAI/client/{Desktop,Mobile}).

Shared message shape (used by ReceiveTutorConvo.initialMessages and ReceiveTutorMessage):

{ role = "user" | "ai", content = string, metadata = { id = string?, ... }, temporary = boolean? }

StartTutorConversationClientToServerEvent

  • Direction: Client → Server (fire-and-forget)
  • Fired by: the tutor UI on open and on "new conversation" — @features/TutorAI/client/{Desktop,Mobile}.
  • Received by: @features/TutorAI/server/initstartConversation(player, "user").
  • Payload: none
  • Why: Creates/resets the player's ConversationManager. The server replies via ReceiveTutorConvo.

SendTutorMessageClientToServerEvent

  • Direction: Client → Server (fire-and-forget)
  • Fired by: the tutor UI when the user sends a message — @features/TutorAI/client/{Desktop,Mobile} (also optimistically appends the user's message locally).
  • Received by: @features/TutorAI/server/initsendUserMessage(player, content).
  • Payload: (content: string)
  • Why: Feeds the prompt into the LLM conversation. The server toggles ReceiveTutorTyping and streams the reply via ReceiveTutorMessage.

RateTutorMessageClientToServerEvent

  • Direction: Client → Server (fire-and-forget)
  • Fired by: the message bubble thumbs up/down — @ui/Components/TutorMessages (and required by @ui/Components/MessageBubble).
  • Received by: @features/TutorAI/server/initConversation:voteOnMessage.
  • Payload: (message_id: string, vote: number)
  • Why: Captures per-message quality feedback for the AI tutor (used in analytics).

ReceiveTutorTypingServerToClientEvent

  • Direction: Server → Client
  • Fired by: @features/TutorAI/server/init (true before generating, false on exit/finish).
  • Received by: @features/TutorAI/client/{Desktop,Mobile}.
  • Payload: (typing: boolean)
  • Why: Drives the "AI is typing…" indicator while the LLM generates a response.

ReceiveTutorMessageServerToClientEvent

  • Direction: Server → Client
  • Fired by: @features/TutorAI/server/ConversationManager (real AI reply with metadata, and replicateClientOnlyMessage for errors/system notices).
  • Received by: @features/TutorAI/client/{Desktop,Mobile}appendMessage(message).
  • Payload: a single message table { role = "ai", content, metadata, temporary? }. Content is Roblox-text-filtered server-side; if >40% gets filtered, a canned AI_ROBLOX_FILTER_REPLY is sent instead.
  • Why: Streams each AI/system message to the player's tutor view as it's produced.

ReceiveTutorConvoServerToClientEvent

  • Direction: Server → Client
  • Fired by: @features/TutorAI/server/ConversationManager (after the access/subscription check, with a fallback if validation fails).
  • Received by: @features/TutorAI/client/{Desktop,Mobile}.
  • Payload:
    {
      initialMessages = { <message>, ... },  -- user + non-empty AI messages
      hasAccess       = boolean,             -- allowed to use the tutor at all
      isSubscribed    = boolean,             -- premium subscription state
    }
    
  • Why: Sends the full conversation snapshot plus access/subscription gating when a conversation starts (or resumes), so the client can render history and gate UI.

9. Profile — @features/Profile

GetProfileServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: anywhere a profile is viewed — @features/Profile/client/{Desktop,Mobile,ClassroomDesktop}/ProfileView, @features/Multiplayer/client/Desktop/Invites.
  • Received by: @features/Profile/server/init.
  • Request payload: (ProfileId: number) — a target player's UserId; returns {} if not a number.
  • Response:
    {
      bio        = string,  -- the target's bio, Roblox-text-filtered for the requester
      pose       = any,     -- avatar pose cosmetic
      frame      = any,     -- avatar frame cosmetic
      reputation = number,  -- target's reputation total
    }
    
    Built from a DataStore2.ReadOnly("settings", ProfileId) read plus GlobalStorage reputation; the read-only stores are cleaned up 60s later (a short cache).
  • Why: Lets any player view another player's public profile, with the bio filtered against the viewer's moderation context.

10. Settings — @features/Settings

Per-player client settings (theme, toggles, …) persisted to the "settings" DataStore2 store. Handled by @features/Settings/server/init; the client side is the shared settings module @ui/settings.

GetSavedSettingsServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @ui/settings (Load()).
  • Received by: @features/Settings/server/init.
  • Request payload: none
  • Response: the saved settings table (defaults {}).
  • Why: Restores the player's saved settings on startup.

SetSavedSettingsClientToServerEvent

  • Direction: Client → Server (fire-and-forget)
  • Fired by: @ui/settings (Save()) — serializes the reactive settings into a plain table.
  • Received by: @features/Settings/server/init.
  • Payload: (Settings: table) — rejected if not a table.
  • Why: Persists settings changes.

11. Onboarding — @features/Onboarding

Out-of-box-experience (OOBE) onboarding-tip tracking, backed by the "oobe" DataStore2 store.

GetViewedOOBEServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @features/Onboarding/client/Desktop.
  • Received by: @features/Onboarding/server/init.
  • Request payload: none
  • Response: a { [key: string]: boolean } map (defaults {}), describing which onboarding tips/tooltips the player has already seen.
  • Why: Lets the OOBE handler skip tips the player has already dismissed.

SetViewedOOBEClientToServerEvent

  • Direction: Client → Server (fire-and-forget)
  • Fired by: @features/Onboarding/client/Desktop — on change of each OOBE state (easter-egg states are not saved).
  • Received by: @features/Onboarding/server/init.
  • Payload: (key: string, value: boolean)
  • Why: Marks an onboarding tip as viewed so it won't show again next session.

12. Monetization — @features/Monetization

Robux donations (with a live feed), gamepass state, and coin-purchased cosmetic assets (avatar poses/frames). Donations/assets in @features/Monetization/server/init; gamepasses in the context-split @shared/Data/Gamepasses.

DonationFeedEventServerToClientEvent

  • Direction: Server → Client(s)
  • Fired by: @features/Monetization/server/init (replays the stored feed to a player on join) and @kernel/MessagesHandler (:SendToAllPlayers(...) from a cross-server MessagingService "MessageFeed").
  • Received by: @features/Monetization/client/{Desktop,Mobile}/DonateMenu.
  • Payload: a feed packet{ Type = "DonationFeed", Username, Amount, Time }.
  • Why: Drives the live donation feed/leaderboard (and replays recent donations to joiners).

DonationCompleteServerToClientEvent

  • Direction: Server → Client
  • Fired by: @features/Monetization/server/init — after a donation product receipt is processed.
  • Received by: @features/Monetization/client/{Desktop,Mobile}/DonateMenu.
  • Payload: none
  • Why: Signals the donate UI that the player's donation went through (thank-you/celebration).

GetGamepassesServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @shared/Data/Gamepasses/Client (loadGamePasses, retries on failure).
  • Received by: @shared/Data/Gamepasses/Server.
  • Request payload: none
  • Response: a { [passName] = true } map for the gamepasses the player owns (defaults {}).
  • Why: Loads owned gamepasses on startup (gates premium features like "Beta Tester").

UpdateGamepassesServerToClientEvent

  • Direction: Server → Client
  • Fired by: @shared/Data/Gamepasses/Server (initial computed set, and after a purchase is detected).
  • Received by: @shared/Data/Gamepasses/Client.
  • Payload: (PlayerPasses) — the { [passName] = true } map.
  • Why: Pushes gamepass ownership changes live (e.g. right after a purchase).

GetOwnedAssetsServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: the app shells — @app/{DesktopApp,MobileApp,ClassroomDesktopApp}.
  • Received by: @features/Monetization/server/init.
  • Request payload: none
  • Response: the player's "owned_assets" store — { [assetId] = true } (defaults {}).
  • Why: Loads which cosmetic assets (poses/frames) the player owns on startup.

PurchaseAssetServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: the profile cosmetic UI — @features/Settings/client/{Desktop,Mobile}/Profile.
  • Received by: @features/Monetization/server/init.
  • Request payload: (assetId) — a pose or frame id (looked up in Poses.Dict / Frames.Dict).
  • Response: { success, data }. Failure data: "Invalid asset ID", "Not enough coins". On success, deducts the asset's Cost, marks it owned, returns "Purchase successful".
  • Why: Buys a cosmetic avatar asset with coins.

13. Feedback — @features/Feedback

User feedback / bug reports relayed to a Slack workspace via a SlackBot, with per-user threading. Server logic in @features/Feedback/server/init; UI in @features/Feedback/client/{Desktop,Mobile}. Channels are "Feedback" and "Bug Report".

GetThreadServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @features/Feedback/client/{Desktop,Mobile}.
  • Received by: @features/Feedback/server/init.
  • Request payload: (channel: string)"Feedback" or "Bug Report" (else "Invalid channel").
  • Response: the Slack thread's messages array (the player's prior conversation), {} if none yet, or an error string. Cached by thread_ts.
  • Why: Loads the player's existing feedback/bug-report conversation so they can see replies.

SendMessageServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @features/Feedback/client/{Desktop,Mobile} (with a "Desktop"/"Mobile" platform tag).
  • Received by: @features/Feedback/server/init.
  • Request payload: (message: string, channel: string, platform: string)
  • Validation / returns (status string): cooldown, >600 chars, <15 chars / no word chars, duplicate, email/Discord-handle guards, else "Message sent!" / "Message failed to send...". Posts to Slack (with the user's profile link + platform + Beta Tester tag), threaded per user.
  • Why: Sends a feedback/bug-report message to the team's Slack, threaded per user.

14. IDE — @features/IDE

GetLoadstringErrorsServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @ui/Components/IDE/ErrorHighlighter — debounced, on the editor's current text.
  • Received by: @features/IDE/server/init.
  • Request payload: (code: string)
  • Response: false if the code compiles, otherwise the loadstring error string.
  • Why: Powers live syntax-error squiggles in the code editor (server-side loadstring, since the client can't loadstring).

15. Deeplinking — @features/Deeplinking

  • Direction: Client → Server (RPC)
  • Fired by: the app shells ~2s after load — @app/DesktopApp, @app/MobileApp (the Classroom app has this commented out).
  • Received by: @features/Deeplinking/server/init.
  • Request payload: none
  • Response: the player's launch data table (or {}). Decoded server-side from Player:GetJoinData().LaunchData (JSON) on join. Shape: { section: string?, id: string? } where id for "Lessons" is "<chapter>,<lesson>" and for "Tutorials" is a tutorial id.
  • Why: Lets external links (Roblox launch data) deep-link a player straight into a specific section / tutorial / lesson. The client sets sharedState.openSection, openTutorial, or currentLesson.

16. Moderation — @features/Moderation

The in-app moderation console (for staff with Permissions.Submission / Permissions.Admin): a password-gated login, then a queue to accept/reject/publish user tutorial submissions. The two credential remotes are EncryptedNet-wrapped. Privileged calls carry a sessionKey (issued by AttemptLogin) that the server re-validates. Server logic in @features/Moderation/server/init; UI in @features/Moderation/client/Desktop ({init, LoginScreen, RegisterScreen, ModTools}).

Submission queue stores: unsortedStore (awaiting review) → acceptableStore (accepted, awaiting publish) → tutorialStore (published). Accept/Reject are queued and flushed in batches (~6s), which also notifies authors and re-broadcasts the queue to moderators.

GetLoginStatusServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: @features/Moderation/client/Desktop@features/Moderation/server/init.
  • Request payload: none
  • Response: "" (no permission), "Login" (registered, needs to log in), or "Register" (has permission but no password set yet).
  • Why: Decides whether to show the moderator the login, the register, or nothing.

ReceiveLoginStatusServerToClientEvent

  • Direction: Server → Client
  • Fired by: @features/Moderation/server/init ("Login" after register, "LoggedIn" after successful login).
  • Received by: @features/Moderation/client/Desktop.
  • Payload: (status: string)"Login" | "LoggedIn".
  • Why: Advances the moderator UI's auth state.

AttemptRegisterServerAsyncFunction (EncryptedNet)

  • Direction: Client → Server (RPC, encrypted+signed)
  • Fired by: @features/Moderation/client/Desktop/RegisterScreen.
  • Received by: @features/Moderation/server/init.
  • Request payload: (Password: string)
  • Response: { success, msg }. On success, sends ReceiveLoginStatus("Login").
  • Why: Sets a moderator's password for the first time.

AttemptLoginServerAsyncFunction (EncryptedNet)

  • Direction: Client → Server (RPC, encrypted+signed)
  • Fired by: @features/Moderation/client/Desktop/LoginScreen.
  • Received by: @features/Moderation/server/init.
  • Request payload: (Password: string)
  • Response: { success, msg, sessionKey? }. On success, sends ReceiveLoginStatus("LoggedIn") and returns a sessionKey (used by all subsequent privileged calls). After 5 incorrect attempts the player is kicked.
  • Why: Authenticates a moderator and issues their session key.

GetUnsortedSubmissionsServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: @features/Moderation/client/Desktop/ModTools@features/Moderation/server/init.
  • Request payload: (sessionKey: string) — requires "Submission" permission (returns {} otherwise).
  • Response: the full unsortedStore map of submissions awaiting review.
  • Why: Loads the queue of un-reviewed tutorial submissions.

GetAcceptableSubmissionsServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: @features/Moderation/client/Desktop/ModTools@features/Moderation/server/init.
  • Request payload: (sessionKey: string) — requires "Submission" permission.
  • Response: the acceptableStore map (accepted submissions awaiting publish).
  • Why: Loads the queue of accepted-but-not-yet-published submissions.

BroadcastSubmissionsServerToClientEvent

  • Direction: Server → Clients (logged-in moderators)
  • Fired by: @features/Moderation/server/init:SendToPlayers(moderators, {...}) after each accept/reject flush.
  • Received by: @features/Moderation/client/Desktop/ModTools.
  • Payload: { Unsorted = <unsortedStore set>, Acceptable = <acceptableStore set> }
  • Why: Live-syncs the moderation queues across all logged-in moderators after changes.

AcceptSubmissionServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: @features/Moderation/client/Desktop/ModTools@features/Moderation/server/init.
  • Request payload: (sessionKey: string, tutorialId: string) — requires "Submission" permission.
  • Response: true/false. Queues the submission for acceptance (unsorted → acceptable on next flush).
  • Why: Approves a submission into the "acceptable" pool.

RejectSubmissionServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by / Received by: @features/Moderation/client/Desktop/ModTools@features/Moderation/server/init.
  • Request payload: (sessionKey: string, tutorialId: string) — requires "Submission" permission.
  • Response: true/false. Queues a rejection; on flush, sets the author's submission status to "Rejected Draft" and notifies them (with a SubmissionLink deep-link).
  • Why: Rejects a submission and informs the author.

PublishSubmissionServerAsyncFunction (Admin-only)

  • Direction: Client → Server (RPC)
  • Fired by / Received by: @features/Moderation/client/Desktop/ModTools@features/Moderation/server/init.
  • Request payload: (sessionKey: string, tutorialId: string) — requires "Admin" permission.
  • Response: true/false. Moves the submission into the live tutorialStore (preserving existing awards if already published), removing it from both pending queues.
  • Why: Publishes an accepted submission live as a real tutorial (admin gate).

17. Classroom — @features/Classroom

Classroom mode runs in private servers for schools: a password login gate, an activity log, a discussion board, and assignments/gradebook. Every classroom callback first checks Security:IsLoggedIn(Player), and actions are written to the ActivityLog. Server logic is split across @features/Classroom/server/{Security,ActivityLog,Discussions,Assignments}. The UI lives in the @app/ClassroomDesktopApp shell (the classroom apps are app-shell variants, not a client slice).

17.1 Security & activity log

The two credential-bearing remotes are EncryptedNet-wrapped (see §1.3).

GetClassroomOwnerServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @app/ClassroomDesktopApp.
  • Received by: @features/Classroom/server/Security.
  • Request payload: none
  • Response: the classroom owner's UserId.
  • Why: Lets the client know who the teacher/owner is (drives teacher-vs-student UI).

GetClassroomRegisteredServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @app/ClassroomDesktopApp.
  • Received by: @features/Classroom/server/Security.
  • Request payload: none
  • Response: { success = true, isRegistered = boolean } — whether this classroom has a password set yet.
  • Why: Decides whether to show the teacher the "set a password" setup or the student login.

AttemptClassroomLoginServerAsyncFunction (EncryptedNet)

  • Direction: Client → Server (RPC, encrypted+signed)
  • Fired by: @app/ClassroomDesktopApp/AppComponents/StartupScreen/{StudentSetup,SecurityLock}.
  • Received by: @features/Classroom/server/Security.
  • Request payload: (Password: string)
  • Response: { msg = <result>, sessionKey = <key>? }. On success logs a PlayerJoined activity and returns a session key.
  • Why: Authenticates a student into the classroom with the teacher-set password.

RegisterClassroomPasswordServerAsyncFunction (EncryptedNet)

  • Direction: Client → Server (RPC, encrypted+signed)
  • Fired by: @app/ClassroomDesktopApp/AppComponents/StartupScreen/TeacherSetup.
  • Received by: @features/Classroom/server/Security.
  • Request payload: (Password: string)
  • Response: { success, msg }. Only the server owner may set it; logs a ClassroomPasswordSet activity on success.
  • Why: Lets the teacher (server owner) set the classroom's login password.

GetClassroomActivityLogsServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @app/ClassroomDesktopApp/AppComponents/DashboardSection.
  • Received by: @features/Classroom/server/ActivityLog.
  • Request payload: (showingTimespan: "Hour" | "Day" | "Week" | "Month" | "All")
  • Response: { success, data?, message? }. The owner gets all logs; a student gets only their own entries (server-side filtered by userId).
  • Why: Powers the teacher dashboard's activity feed (with student-scoped privacy).

17.2 Discussions

A classroom discussion board (posts + threaded comments). Structurally mirrors the public tutorial+comment system, but classroom-scoped (login required) and activity-logged. All in @features/Classroom/server/Discussions; UI in @app/ClassroomDesktopApp/AppComponents/DiscussionsSection. Responses use { success, message, data? }.

GetDiscussionMetadatasServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @app/ClassroomDesktopApp/AppComponents/DiscussionsSection.
  • Received by: @features/Classroom/server/Discussions.
  • Request payload: none
  • Response: per-post metadata { Key, AuthorId, Title, OriginalPostDate, LatestReplyDate } (no body). Requires login.
  • Why: Lists discussion threads in the board.

UpdateDiscussionsMetadatasServerToClientEvent

  • Direction: Server → Client (logged-in classroom members)
  • Fired by: @features/Classroom/server/Discussions — on OnChunkChanged, pushed to each player with an active session.
  • Received by: @app/ClassroomDesktopApp/AppComponents/DiscussionsSection.
  • Payload: (processedChunk) — post metadata map.
  • Why: Live-updates the discussion list as posts change.

GetDiscussionContentServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @app/ClassroomDesktopApp/AppComponents/DiscussionsSection (+ its DiscussionsReader).
  • Received by: @features/Classroom/server/Discussions.
  • Request payload: (discussionId: string)
  • Response: { success, message, data? } — the Roblox-text-filtered post Content. Logs a ViewedDiscussion activity. Requires login.
  • Why: Opens a discussion post's body.

PostDiscussionServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @app/ClassroomDesktopApp/AppComponents/DiscussionsSection.
  • Received by: @features/Classroom/server/Discussions.
  • Request payload: (discussionId: string, post: { Title, Content }) — creates a new post or edits an existing one (author-only edit).
  • Response: { success, message, data? }. Logs PostedDiscussion. Requires login.
  • Why: Creates or edits a discussion thread.

DeleteDiscussionServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @app/ClassroomDesktopApp/AppComponents/DiscussionsSection.
  • Received by: @features/Classroom/server/Discussions.
  • Request payload: (discussionId: string)
  • Response: { success, message }. Only the author or classroom owner may delete. Removes both the post and its comment tree; logs DeletedDiscussion. Requires login.
  • Why: Deletes a discussion thread (author or teacher).

17.3 Discussion comments

Threaded comments on classroom discussions — the same two-phase MPTT loading and validation as §5.3, but classroom-gated and activity-logged. All in @features/Classroom/server/Discussions; UI in @app/ClassroomDesktopApp/AppComponents/DiscussionsSection/DiscussionsReader.

GetDiscussionCommentsServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: DiscussionsReader@features/Classroom/server/Discussions.
  • Request payload: (key: string) — the discussion id.
  • Response: { success = true, data = <MPTT set> } with each node's Content/AuthorId stripped. Requires login.
  • Why: Loads a discussion's comment tree skeleton.

PopulateDiscussionCommentServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: DiscussionsReader@features/Classroom/server/Discussions.
  • Request payload: (key: string, nodeId: string)
  • Response: { success = true, data = { Content, AuthorId } }Content filtered for the requester. Requires login.
  • Why: Lazily fetches the full content of one comment node.

PostDiscussionCommentServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: DiscussionsReader@features/Classroom/server/Discussions.
  • Request payload: (discussionId: string, targetId: string, content: string)
  • Validation: content 20–800 chars, plus the same email/Discord/filter guards as tutorial comments. Requires login.
  • Response: { success, message, ... }.
  • Why: Adds a comment/reply to a discussion thread.

EditDiscussionCommentServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: DiscussionsReader@features/Classroom/server/Discussions.
  • Request payload: (discussionId: string, targetId: string, content: string) — author-only, same validation.
  • Response: { success, message }. Requires login.
  • Why: Edits one's own discussion comment.

DeleteDiscussionCommentServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: DiscussionsReader@features/Classroom/server/Discussions.
  • Request payload: (discussionId: string, nodeId: string)
  • Response: { success, message }. message: "Comment not found", "Already deleted", "You do not have permission to delete this comment" (author or Permissions.Comments), or "Deleted successfully!". Logs DeleteDiscussionComment. Requires login.
  • Why: Removes a comment from a discussion thread.

17.4 Assignments

Classroom assignments / gradebook. An assignment has shared Info (teacher-authored) and per-student UserInfo. Every callback requires Security:IsLoggedIn; teacher-only actions additionally require Player.UserId == ClassroomUtil.GetClassroomOwner(). Server logic in @features/Classroom/server/Assignments; UI in the AssignmentsSection (student) and GradebookSection (teacher) under @app/ClassroomDesktopApp.

ListAssignmentsServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @app/ClassroomDesktopApp.
  • Received by: @features/Classroom/server/Assignments.
  • Request payload: none
  • Response: { success, message, data? } where data = { [id] = { ID, Info, UserInfo } }. Hidden assignments are omitted for non-owners.
  • Why: Loads the student's (or teacher's) assignment list with their progress.

GetAssignmentInfoServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @app/ClassroomDesktopApp/AppComponents/{AssignmentsSection,GradebookSection}.
  • Received by: @features/Classroom/server/Assignments.
  • Request payload: (assignmentId: string)
  • Response: { success, message, data? } — the shared assignment Info. Logs a ViewedAssignment activity.
  • Why: Loads a single assignment's shared details (title, blocks, deadlines).

GetAssignmentUserInfoServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @app/ClassroomDesktopApp/AppComponents/{AssignmentsSection,GradebookSection}.
  • Received by: @features/Classroom/server/Assignments.
  • Request payload: (assignmentId: string, userId?) — defaults to the caller; viewing another user requires being the owner.
  • Response: { success, message, data? } — the per-user assignment data (may be nil if nothing saved yet).
  • Why: Loads a student's submission/marks (own data, or any student's for the teacher).

EditAssignmentInfoServerAsyncFunction (teacher-only)

  • Direction: Client → Server (RPC)
  • Fired by: @app/ClassroomDesktopApp/AppComponents/AssignmentsSection.
  • Received by: @features/Classroom/server/Assignments.
  • Request payload: (assignmentId: string, editedInfo: table)
  • Response: { success, message, ... }. Requires being the classroom owner.
  • Why: Lets the teacher edit an assignment's shared content/settings.

EditAssignmentUserInfoServerAsyncFunction

  • Direction: Client → Server (RPC)
  • Fired by: @app/ClassroomDesktopApp/AppComponents/AssignmentsSection (student saving own work) and .../GradebookSection/GradeBreakdown/AssignmentBreakdown (teacher grading).
  • Received by: @features/Classroom/server/Assignments.
  • Request payload: (assignmentId: string, userId?, editedUserInfo: table) — a student may only edit their own; the teacher may edit any. Typechecked against TYPE_EDITABLE_USER_INFO.
  • Response: { success, message, ... }.
  • Why: Saves a student's work, or the teacher's grades/feedback, on an assignment.

CreateAssignmentServerAsyncFunction (teacher-only)

  • Direction: Client → Server (RPC)
  • Fired by: @app/ClassroomDesktopApp/AppComponents/AssignmentsSection.
  • Received by: @features/Classroom/server/Assignments.
  • Request payload: none
  • Response: { success, message, id? }. Owner-only; creates a hidden "Untitled Assignment" and logs CreatedAssignment.
  • Why: Lets the teacher create a new (hidden) assignment to fill in.

DeleteAssignmentServerAsyncFunction (teacher-only)

  • Direction: Client → Server (RPC)
  • Fired by: @app/ClassroomDesktopApp/AppComponents/AssignmentsSection.
  • Received by: @features/Classroom/server/Assignments.
  • Request payload: (assignmentId: string)
  • Response: { success, message }. Owner-only.
  • Why: Lets the teacher delete an assignment.

Appendix — notes & caveats

  • ReadAllNotifications (§2.4) is defined and handled server-side but currently has no active client caller; treat it as reserved.
  • The AI tutor reads GetTutorialContent / GetQuestContent traffic via an extra OnServerEvent listener on the remotes' underlying instances (@features/TutorAI/server/Util) — a second consumer of those ServerAsyncFunctions beyond their SetCallback. This works because vorlias/net implements async functions over RemoteEvents.
  • SectionTimeAnalytics (§3) and ViewLesson (§4) each have two independent server listeners (their feature handler + the AI-tutor context tracker in @features/TutorAI/server/Util).
  • DonationFeedEvent (§12) is sent from two places: the Monetization server (join replay) and @kernel/MessagesHandler (cross-server feed). GetServerType, FilterText, the notification remotes, and the economy/bookmarks remotes are declared in Core but handled by @kernel/PlayerData.
  • Encrypted remotes (AttemptLogin, AttemptRegister, AttemptClassroomLogin, RegisterClassroomPassword) are the only ones wrapped with EncryptedNet; everything else is plain vorlias/net.