Skip to content

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

  1. Single source of truth: Remote is defined once in registry
  2. Schema validation: Server validates before processing
  3. Rate limiting: Handled by middleware (not shown, but applied)
  4. Kill-switch: Feature flag checked before any logic
  5. Stable errors: Returns ErrorCode, never throws
  6. 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 retried
  • matchId: string, required for match-scoped actions
  • seq: 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: boolean
  • code: number (stable)
  • retryAfterMs?: number
  • message?: 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→S client to server, S→C server to client
  • Budget: per player unless stated

Session & compatibility

  1. Net.Handshake (C→S, request/response)
  2. Purpose: protocol compatibility and server capabilities
  3. Payload:
    • protocolVersion: number
    • buildId: string
    • deviceClass: "kbm"|"gamepad"|"touch"
  4. Server validates:
    • protocol range check
    • device enum
  5. Budget: 1 per join
  6. Errors:

    • 3001 PROTOCOL_INCOMPATIBLE
  7. Config.GetSnapshot (C→S, request/response)

  8. Purpose: deliver a validated config snapshot (non-secret)
  9. Payload:
    • knownConfigVersion?: string
  10. Budget: 1 per join + manual refresh (cooldown 30s)

Real-time match traffic

  1. Match.Input (C→S, event)
  2. Purpose: batched input command stream
  3. Payload (batched):
    • matchId: string
    • seqStart: number
    • commands: InputCommand[]
  4. InputCommand:
    • seq: number
    • dtMs: number (clamped)
    • moveX: number, moveY: number
    • lookYaw: number, lookPitch: number (clamped)
    • jump: boolean, sprint: boolean, crouch: boolean
  5. Budget:
    • 20 Hz recommended (device dependent)
    • max commands per packet (e.g. 4)
  6. Abuse handling:

    • drop packets beyond budget
    • increment security score if persistent
  7. Match.Snapshot (S→C, event)

  8. Purpose: authoritative state snapshots
  9. Payload:
    • matchId: string
    • serverTick: number
    • entities: EntityState[] (bounded)
  10. Notes:
    • clients interpolate; corrections are smoothed

Combat & abilities

  1. Combat.Fire (C→S, request/ack)
  2. Purpose: propose firing intent (server validates hit)
  3. Payload:
    • matchId: string
    • seq: number
    • weaponId: string
    • origin: Vector3 (clamped to player muzzle bounds)
    • direction: Vector3 (normalized, clamped)
    • clientTimeMs: number
  4. Budget:
    • aligned to weapon fire rate; enforce on server
  5. Server validates:
    • weapon equipped
    • ammo + cooldown
    • origin plausibility
    • optional lag-comp window
  6. Errors:

    • 2101 COOLDOWN
    • 2102 NO_AMMO
    • 2103 INVALID_STATE
  7. Combat.ActivateAbility (C→S, request/ack)

  8. Purpose: ability activation intent
  9. Payload:
    • abilityId: string
    • target?: TargetRef (bounded)
  10. Budget:
    • per-ability

Matchmaking

  1. Queue.Join (C→S, request/response)
  2. Payload:
    • mode: string (e.g. ranked_2v2)
    • partyId?: string
    • regionPreference?: string
    • deviceClass: "kbm"|"gamepad"|"touch"
  3. Budget: low (cooldown 2s)

  4. Queue.Leave (C→S, request/response)

  5. Budget: low

  6. Queue.MatchFound (S→C, event)

  7. Payload:
    • ticketId: string
    • teleportData: object (non-secret)

Moderation

  1. Report.Player (C→S, request/response)

  2. Payload:

  3. reportedUserId: number
  4. reasonCode: string
  5. freeform?: string (length limited)
  6. Budget: strict (cooldown 30s)

  7. Admin.Command (C→S, request/response)

  8. Purpose: in-experience admin actions for authorized moderators

  9. Must be RBAC gated server-side
  10. Budget: very strict

Telemetry

  1. Telemetry.EventBatch (C→S, event)

  2. Purpose: client telemetry in batches

  3. Budget: strict; sample heavily
  4. Notes:
  5. must never include secrets or personal data

Error code ranges (recommendation)

  • 1xxx: validation (schema, bounds)
  • 2xxx: gameplay (cooldowns, state)
  • 3xxx: compatibility (protocol)
  • 4xxx: authz/admin
  • 5xxx: 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)