Architecture: Networking schema catalog¶
This page is the living catalog of every planned network message (remote), including payload shape, validation rules, budgets, and error codes.
Golden path example (Phase 1)¶
This example shows how a remote is defined, validated, and handled. Use this as a template for all new remotes.
1. Type Definition (Shared)¶
// packages/shared-types/src/index.ts
export interface DoActionPayload {
actionId: string;
timestamp: number;
}
2. Validation Schema (Net)¶
// packages/net/src/validation.ts
import { t } from "@rbxts/t";
import { validate, bounded } from "@broblox/net";
const doActionSchema = t.strictInterface({
actionId: bounded.string(50, 1),
timestamp: t.number,
});
export function validateDoActionPayload(value: unknown): Result<DoActionPayload> {
return validate(doActionSchema, value);
}
3. Remote Registry (Net)¶
// packages/net/src/remotes.ts
export const REMOTES = {
DoAction: {
name: "Intent_DoAction",
rateLimit: { windowMs: 1000, maxRequests: 5 },
},
} as const;
Server handler¶
// games/test-park/src/server/services/ActionService.ts
import { REMOTES, validateDoActionPayload, ErrorCode, Result } from "@broblox/net";
import { isFlagEnabled } from "@broblox/config-featureflags";
const playerState = new Map<number, number>();
export function handleDoAction(player: Player, rawPayload: unknown): Result<{ newCount: number }> {
// 1. Check kill-switch
if (!isFlagEnabled("doAction.enabled")) {
return { ok: false, code: ErrorCode.FeatureDisabled };
}
// 2. Validate payload
const validation = validateDoActionPayload(rawPayload);
if (!validation.ok) {
// Log violation for security scoring
logSecurityEvent("invalid_payload", { player, remote: "Intent_DoAction" });
return { ok: false, code: ErrorCode.InvalidPayload };
}
const payload = validation.value;
// 3. Bounds check (defense in depth)
if (payload.timestamp < 0 || payload.timestamp > os.clock() * 1000 + 5000) {
return { ok: false, code: ErrorCode.InvalidPayload };
}
// 4. Apply server-authoritative state change
const currentCount = playerState.get(player.UserId) ?? 0;
const newCount = currentCount + 1;
playerState.set(player.UserId, newCount);
// 5. Return success
return { ok: true, value: { newCount } };
}
Client caller¶
// games/test-park/src/client/controllers/ActionController.ts
import { RemoteController } from "./RemoteController";
import { type Result, type DoActionPayload } from "@broblox/net";
const payload: DoActionPayload = {
actionId: "intent_ping",
timestamp: os.clock() * 1000,
};
const result = RemoteController.Remotes.DoAction.InvokeServer(payload) as Result<{
actionId: string;
processedAt: number;
}>;
if (!result.ok) {
warn(`Action failed: ${result.code}`);
}
Key patterns demonstrated¶
- Single source of truth: Remote is defined once in registry
- Schema validation: Server validates before processing
- Rate limiting: Handled by middleware (not shown, but applied)
- Kill-switch: Feature flag checked before any logic
- Stable errors: Returns
ErrorCode, never throws - Server authority: State change is server-side only
Global rules (apply to all remotes)¶
- All inbound payloads are schema-validated on the server.
- All inbound payloads are bounded (max sizes, max arrays, clamped numbers).
- All inbound remotes are rate-limited (per player + per endpoint).
- All remotes return stable error codes; no throws across the boundary.
- The server accepts intent, never outcomes.
Common envelope conventions¶
Identifiers¶
requestId: string, required for any request that can be retriedmatchId: string, required for match-scoped actionsseq: integer, monotonic per stream (input, fire requests)clientTimeMs: integer, for lag-comp proposals (bounded window)
Error response¶
For request/response calls (or server acknowledgements):
ok: booleancode: number(stable)retryAfterMs?: numbermessage?: string(dev only; never leak internal server details)
Rate limit policy (baseline)¶
- Hard real-time (input stream): fixed frequency (no burst); drop excess.
- Action (fire/ability): token bucket with cooldown semantics.
- Admin: very low rate + RBAC enforced.
Endpoint catalog (v1 target)¶
Notation:
- Direction:
C→Sclient to server,S→Cserver to client - Budget: per player unless stated
Session & compatibility¶
Net.Handshake(C→S, request/response)- Purpose: protocol compatibility and server capabilities
- Payload:
protocolVersion: numberbuildId: stringdeviceClass: "kbm"|"gamepad"|"touch"
- Server validates:
- protocol range check
- device enum
- Budget: 1 per join
-
Errors:
3001 PROTOCOL_INCOMPATIBLE
-
Config.GetSnapshot(C→S, request/response) - Purpose: deliver a validated config snapshot (non-secret)
- Payload:
knownConfigVersion?: string
- Budget: 1 per join + manual refresh (cooldown 30s)
Real-time match traffic¶
Match.Input(C→S, event)- Purpose: batched input command stream
- Payload (batched):
matchId: stringseqStart: numbercommands: InputCommand[]
InputCommand:seq: numberdtMs: number(clamped)moveX: number,moveY: numberlookYaw: number,lookPitch: number(clamped)jump: boolean,sprint: boolean,crouch: boolean
- Budget:
- 20 Hz recommended (device dependent)
- max commands per packet (e.g. 4)
-
Abuse handling:
- drop packets beyond budget
- increment security score if persistent
-
Match.Snapshot(S→C, event) - Purpose: authoritative state snapshots
- Payload:
matchId: stringserverTick: numberentities: EntityState[](bounded)
- Notes:
- clients interpolate; corrections are smoothed
Combat & abilities¶
Combat.Fire(C→S, request/ack)- Purpose: propose firing intent (server validates hit)
- Payload:
matchId: stringseq: numberweaponId: stringorigin: Vector3(clamped to player muzzle bounds)direction: Vector3(normalized, clamped)clientTimeMs: number
- Budget:
- aligned to weapon fire rate; enforce on server
- Server validates:
- weapon equipped
- ammo + cooldown
- origin plausibility
- optional lag-comp window
-
Errors:
2101 COOLDOWN2102 NO_AMMO2103 INVALID_STATE
-
Combat.ActivateAbility(C→S, request/ack) - Purpose: ability activation intent
- Payload:
abilityId: stringtarget?: TargetRef(bounded)
- Budget:
- per-ability
Matchmaking¶
Queue.Join(C→S, request/response)- Payload:
mode: string(e.g.ranked_2v2)partyId?: stringregionPreference?: stringdeviceClass: "kbm"|"gamepad"|"touch"
-
Budget: low (cooldown 2s)
-
Queue.Leave(C→S, request/response) -
Budget: low
-
Queue.MatchFound(S→C, event) - Payload:
ticketId: stringteleportData: object(non-secret)
Moderation¶
-
Report.Player(C→S, request/response) -
Payload:
reportedUserId: numberreasonCode: stringfreeform?: string(length limited)-
Budget: strict (cooldown 30s)
-
Admin.Command(C→S, request/response) -
Purpose: in-experience admin actions for authorized moderators
- Must be RBAC gated server-side
- Budget: very strict
Telemetry¶
-
Telemetry.EventBatch(C→S, event) -
Purpose: client telemetry in batches
- Budget: strict; sample heavily
- Notes:
- must never include secrets or personal data
Error code ranges (recommendation)¶
1xxx: validation (schema, bounds)2xxx: gameplay (cooldowns, state)3xxx: compatibility (protocol)4xxx: authz/admin5xxx: server busy/transient
Change process¶
- Any change to this catalog requires an ADR if it breaks compatibility.
- Every new endpoint must specify:
- payload schema
- budget
- abuse handling
- observability event(s)
Game-level remotes (implemented)¶
The following remotes are defined per-game and follow the same golden-path pattern above. They are organized by feature area.
Shared metagame remotes (both games)¶
These remotes exist in both test-park and obby with identical or near-identical payloads.
Client→Server Functions:
| Remote | Payload → Response | Rate Limit | Purpose |
|---|---|---|---|
GetFullPlayerData |
void → FullPlayerDataPayload |
2/2000ms | Fetch full player state after handshake |
ClaimDailyReward |
void → DailyRewardDay \| undefined |
1/2000ms | Claim daily login reward |
RedeemCode |
RedeemCodeRequest → CodeRedeemResultPayload |
1/3000ms | Redeem a promo code |
HatchEgg |
HatchEggRequest → HatchResult[] |
3/1000ms | Gacha pull |
CheckGamePass |
CheckGamePassRequest → GamePassOwnershipPayload |
5/2000ms | Verify game pass ownership |
Client→Server Events:
| Remote | Payload | Rate Limit | Purpose |
|---|---|---|---|
EquipPet |
EquipPetRequest |
3/500ms | Equip a pet |
UnequipPet |
UnequipPetRequest |
3/500ms | Unequip a pet |
EquipCosmetic |
EquipCosmeticRequest |
3/500ms | Equip a cosmetic |
UnequipCosmetic |
UnequipCosmeticRequest |
3/500ms | Unequip a cosmetic |
ClaimBattlePassReward |
ClaimBattlePassRewardRequest |
3/1000ms | Claim a battle pass tier reward |
BuyProduct |
BuyProductRequest |
2/2000ms | Initiate developer product purchase |
Server→Client Events:
| Remote | Payload | Purpose |
|---|---|---|
PlayerDataSync |
PlayerDataSyncPayload |
Broadcast full player data after mutations |
LevelUp |
LevelUpPayload |
Notify client of level-up |
PrestigeUnlocked |
PrestigeUnlockedPayload |
Notify client of prestige unlock |
QuestCompleted |
QuestCompletedPayload |
Notify client of quest completion |
AchievementCompleted |
AchievementCompletedPayload |
Notify client of achievement |
DailyRewardClaimed |
DailyRewardClaimedPayload |
Confirm daily reward claim |
EventStarted |
EventActivePayload |
Notify client that a timed event started |
EventEnded |
EventActivePayload |
Notify client that a timed event ended |
Test-park-specific remotes¶
Client→Server Events:
| Remote | Payload | Rate Limit | Purpose |
|---|---|---|---|
UseAbility |
UseAbilityRequest |
10/200ms | Fire combat ability |
TestPark_Teleport |
TestParkTeleportRequest |
3/1000ms | Teleport to test zone |
Client→Server Functions:
| Remote | Payload → Response | Rate Limit | Purpose |
|---|---|---|---|
ReportHit |
ReportHitRequest → HitResultPayload |
10/200ms | Report hit for server validation |
Server→Client Events:
| Remote | Payload | Purpose |
|---|---|---|
Notification |
ServerNotification |
General server notification |
TestPark_ActionResult |
TestParkActionResultPayload |
Action test result feedback toast |
Obby-specific remotes¶
Client→Server Events:
| Remote | Payload | Rate Limit | Purpose |
|---|---|---|---|
RequestRespawn |
RespawnRequestPayload |
2/1000ms | Request respawn at checkpoint |
RequestLeaderboard |
void |
1/2000ms | Request leaderboard refresh |
RequestTraining |
TrainingRequestPayload |
2/2000ms | Start attribute training |
RequestEnterWorld |
EnterWorldRequestPayload |
2/2000ms | Enter a world zone |
RequestExitWorld |
void |
2/2000ms | Exit current world |
EquipGear |
EquipGearRequest |
3/500ms | Equip a gear item |
UnequipGear |
UnequipGearRequest |
3/500ms | Unequip a gear item |
Client→Server Functions:
| Remote | Payload → Response | Rate Limit | Purpose |
|---|---|---|---|
BuyGear |
BuyGearRequest → BuyGearResultPayload |
2/1000ms | Purchase a gear item |
Server→Client Events:
| Remote | Payload | Purpose |
|---|---|---|
CheckpointReached |
CheckpointReachedEvent |
Notify checkpoint progress |
StageCompleted |
StageCompletedEvent |
Notify stage completion |
LeaderboardUpdate |
LeaderboardUpdatePayload |
Push leaderboard data |
LeaderboardRefreshStatus |
LeaderboardRefreshStatusPayload |
Leaderboard refresh cooldown status |
AttributeSync |
AttributeSyncPayload |
Sync player RPG attributes |
TrainingComplete |
TrainingCompletePayload |
Training session result |
StaminaSync |
StaminaSyncPayload |
Sync stamina state |
WorldChanged |
WorldChangedPayload |
Notify world zone change |
EquipmentSync |
EquipmentSyncPayload |
Sync equipped gear |
HazardToggle |
HazardTogglePayload |
Hazard enabled/disabled state |
HazardDamage |
HazardDamagePayload |
Hazard damage feedback |
ObstacleUpdate |
ObstacleUpdatePayload |
Obstacle position/state update |
ObstacleToggle |
ObstacleTogglePayload |
Obstacle enabled/disabled state |