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 "@rbx/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/starter/src/server/services/ActionService.ts
import { REMOTES, validateDoActionPayload, ErrorCode, Result } from "@rbx/net";
import { isFlagEnabled } from "@rbx/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/starter/src/client/controllers/ActionController.ts
import { RemoteController } from "./RemoteController";
import { type Result, type DoActionPayload } from "@rbx/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)