diff --git a/rivetkit-typescript/CLAUDE.md b/rivetkit-typescript/CLAUDE.md index ae4c13f358..c87d5c92a9 100644 --- a/rivetkit-typescript/CLAUDE.md +++ b/rivetkit-typescript/CLAUDE.md @@ -3,7 +3,7 @@ ## Tree-Shaking Boundaries - Do not import `@rivetkit/workflow-engine` outside the `rivetkit/workflow` entrypoint so it remains tree-shakeable. -- Do not import SQLite VFS or `wa-sqlite` outside the `rivetkit/db` (or `@rivetkit/sqlite-vfs`) entrypoint so SQLite support remains tree-shakeable. +- Do not import SQLite VFS or `@rivetkit/sqlite` outside the `rivetkit/db` (or `@rivetkit/sqlite-vfs`) entrypoint so SQLite support remains tree-shakeable. - Importing `rivetkit/db` (or `@rivetkit/sqlite-vfs`) is the explicit opt-in for SQLite. Do not lazily load SQLite from `rivetkit/db`; it may be imported eagerly inside that entrypoint. - Core drivers must remain SQLite-agnostic. Any SQLite-specific wiring belongs behind the `rivetkit/db` or `@rivetkit/sqlite-vfs` boundary. diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/driver.ts b/rivetkit-typescript/packages/rivetkit/src/actor/driver.ts index d3492b628f..76fa35d656 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/driver.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/driver.ts @@ -69,13 +69,16 @@ export interface ActorDriver { ): Promise | undefined>; /** - * SQLite VFS instance for creating KV-backed databases. + * Returns a SQLite VFS instance for creating KV-backed databases. * If not provided, the database provider will need an override. * - * wa-sqlite's async build is not re-entrant per module instance. Drivers - * should scope this instance to a single actor when using KV-backed SQLite. + * @rivetkit/sqlite's async build is not re-entrant per module instance. Drivers + * should return a new instance per call for actor-level isolation. + * + * This is a method (not a property) so drivers can use dynamic imports, + * keeping the core driver tree-shakeable from @rivetkit/sqlite. */ - sqliteVfs?: SqliteVfs; + getSqliteVfs?(): SqliteVfs | Promise; /** * Requests the actor to go to sleep. diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts b/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts index b528bde144..beaf013455 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/instance/mod.ts @@ -1409,10 +1409,12 @@ export class ActorInstance< let client: InferDatabaseClient | undefined; try { - // Every actor gets its own SqliteVfs/wa-sqlite instance. The async - // wa-sqlite build is not re-entrant, and sharing one instance across + // Every actor gets its own SqliteVfs/@rivetkit/sqlite instance. The async + // @rivetkit/sqlite build is not re-entrant, and sharing one instance across // actors can cause cross-actor contention and runtime corruption. - this.#sqliteVfs ??= this.driver.sqliteVfs; + if (!this.#sqliteVfs && this.driver.getSqliteVfs) { + this.#sqliteVfs = await this.driver.getSqliteVfs(); + } client = await this.#config.db.createClient({ actorId: this.#actorId, diff --git a/rivetkit-typescript/packages/rivetkit/src/db/config.ts b/rivetkit-typescript/packages/rivetkit/src/db/config.ts index 0287c4e1a9..2101154bfe 100644 --- a/rivetkit-typescript/packages/rivetkit/src/db/config.ts +++ b/rivetkit-typescript/packages/rivetkit/src/db/config.ts @@ -36,7 +36,7 @@ export interface DatabaseProviderContext { /** * SQLite VFS instance for creating KV-backed databases. - * This should be actor-scoped because wa-sqlite is not re-entrant per + * This should be actor-scoped because @rivetkit/sqlite is not re-entrant per * module instance. */ sqliteVfs?: SqliteVfs; diff --git a/rivetkit-typescript/packages/rivetkit/src/db/drizzle/mod.ts b/rivetkit-typescript/packages/rivetkit/src/db/drizzle/mod.ts index 3582d51103..255af1ac9c 100644 --- a/rivetkit-typescript/packages/rivetkit/src/db/drizzle/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/db/drizzle/mod.ts @@ -56,8 +56,8 @@ function createActorKvStore(kv: { } /** - * Mutex to serialize async operations on a wa-sqlite database handle. - * wa-sqlite is not safe for concurrent operations on the same handle. + * Mutex to serialize async operations on a @rivetkit/sqlite database handle. + * @rivetkit/sqlite is not safe for concurrent operations on the same handle. */ class DbMutex { #locked = false; @@ -89,7 +89,7 @@ class DbMutex { } /** - * Create a sqlite-proxy async callback from a wa-sqlite Database + * Create a sqlite-proxy async callback from a @rivetkit/sqlite Database */ function createProxyCallback( waDb: Database, @@ -126,7 +126,7 @@ function createProxyCallback( } /** - * Run inline migrations via the wa-sqlite Database. + * Run inline migrations via the @rivetkit/sqlite Database. * Migrations use the same embedded format as drizzle-orm's durable-sqlite. */ async function runInlineMigrations( @@ -186,7 +186,7 @@ export function db< >( config?: DatabaseFactoryConfig, ): DatabaseProvider & RawAccess> { - // Store the wa-sqlite Database instance alongside the drizzle client + // Store the @rivetkit/sqlite Database instance alongside the drizzle client let waDbInstance: Database | null = null; const mutex = new DbMutex(); @@ -240,7 +240,7 @@ export function db< }) as TRow[]; } // Use exec for non-parameterized queries since - // wa-sqlite's query() can crash on some statements. + // @rivetkit/sqlite's query() can crash on some statements. const results: Record[] = []; let columnNames: string[] | null = null; await waDb.exec( diff --git a/rivetkit-typescript/packages/rivetkit/src/db/mod.ts b/rivetkit-typescript/packages/rivetkit/src/db/mod.ts index b0bba20c27..7521752f68 100644 --- a/rivetkit-typescript/packages/rivetkit/src/db/mod.ts +++ b/rivetkit-typescript/packages/rivetkit/src/db/mod.ts @@ -78,8 +78,8 @@ export function db({ let op: Promise = Promise.resolve(); const serialize = async (fn: () => Promise): Promise => { - // Ensure wa-sqlite calls are not concurrent. Actors can process multiple - // actions concurrently, and wa-sqlite is not re-entrant. + // Ensure @rivetkit/sqlite calls are not concurrent. Actors can process multiple + // actions concurrently, and @rivetkit/sqlite is not re-entrant. const next = op.then(fn, fn); op = next.then( () => undefined, diff --git a/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/actor.ts b/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/actor.ts index 4f44f4cd4e..ee2fdd020e 100644 --- a/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/actor.ts +++ b/rivetkit-typescript/packages/rivetkit/src/drivers/file-system/actor.ts @@ -1,6 +1,6 @@ import type { AnyClient } from "@/client/client"; import type { RawDatabaseClient } from "@/db/config"; -import { SqliteVfs } from "@rivetkit/sqlite-vfs"; +import type { SqliteVfs } from "@rivetkit/sqlite-vfs"; import type { ActorDriver, AnyActorInstance, @@ -82,10 +82,18 @@ export class FileSystemActorDriver implements ActorDriver { } /** SQLite VFS instance for creating KV-backed databases */ - get sqliteVfs(): SqliteVfs { - // The async wa-sqlite build is not re-entrant per module instance. + async getSqliteVfs(): Promise { + // Dynamic import keeps @rivetkit/sqlite out of the main entrypoint bundle, + // preserving tree-shakeability for environments that don't use SQLite. + // The async @rivetkit/sqlite build is not re-entrant per module instance. // Returning a fresh SqliteVfs here gives each actor its own module, // allowing actor-level parallelism without cross-actor re-entry. + // + // The specifier is built via concatenation so that bundlers like + // wrangler's esbuild cannot statically analyze and attempt to + // bundle the module (it is never used on Cloudflare Workers). + const specifier = "@rivetkit/" + "sqlite-vfs"; + const { SqliteVfs } = await import(specifier); return new SqliteVfs(); } diff --git a/rivetkit-typescript/packages/rivetkit/src/inspector/actor-inspector.ts b/rivetkit-typescript/packages/rivetkit/src/inspector/actor-inspector.ts index 701229c093..5806aba71f 100644 --- a/rivetkit-typescript/packages/rivetkit/src/inspector/actor-inspector.ts +++ b/rivetkit-typescript/packages/rivetkit/src/inspector/actor-inspector.ts @@ -115,7 +115,7 @@ export class ActorInspector { "SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__drizzle_%'", ) as { name: string; type: string }[]; - // Serialize all queries to avoid concurrent wa-sqlite access + // Serialize all queries to avoid concurrent @rivetkit/sqlite access // which can cause "file is not a database" errors. const tableInfos = []; for (const table of tables) { diff --git a/rivetkit-typescript/packages/sqlite-vfs/src/vfs.ts b/rivetkit-typescript/packages/sqlite-vfs/src/vfs.ts index 1308adab50..d667421b1e 100644 --- a/rivetkit-typescript/packages/sqlite-vfs/src/vfs.ts +++ b/rivetkit-typescript/packages/sqlite-vfs/src/vfs.ts @@ -6,7 +6,7 @@ * used concurrently with other instances. */ -// Note: wa-sqlite VFS.Base type definitions have incorrect types for xRead/xWrite +// Note: @rivetkit/sqlite VFS.Base type definitions have incorrect types for xRead/xWrite // The actual runtime uses Uint8Array, not the {size, value} object shown in types import * as VFS from "@rivetkit/sqlite/src/VFS.js"; @@ -57,7 +57,7 @@ function decodeFileMeta(data: Uint8Array): number { /** * SQLite API interface (subset needed for VFS registration) - * This is part of wa-sqlite but not exported in TypeScript types + * This is part of @rivetkit/sqlite but not exported in TypeScript types */ interface SQLite3Api { vfs_register: (vfs: unknown, makeDefault?: boolean) => number; @@ -88,7 +88,7 @@ interface SQLite3Api { /** * Simple async mutex for serializing database operations - * wa-sqlite calls are not safe to run concurrently on one module instance + * @rivetkit/sqlite calls are not safe to run concurrently on one module instance */ class AsyncMutex { #locked = false; @@ -188,7 +188,7 @@ export class Database { } /** - * Get the raw wa-sqlite API (for advanced usage) + * Get the raw @rivetkit/sqlite API (for advanced usage) */ get sqlite3(): SQLite3Api { return this.#sqlite3; @@ -205,7 +205,7 @@ export class Database { /** * SQLite VFS backed by KV storage. * - * Each instance is independent and has its own wa-sqlite WASM module. + * Each instance is independent and has its own @rivetkit/sqlite WASM module. * This allows multiple instances to operate concurrently without interference. */ export class SqliteVfs { @@ -222,7 +222,7 @@ export class SqliteVfs { } /** - * Initialize wa-sqlite and VFS (called once per instance) + * Initialize @rivetkit/sqlite and VFS (called once per instance) */ async #ensureInitialized(): Promise { // Fast path: already initialized @@ -238,7 +238,7 @@ export class SqliteVfs { const wasmPath = require.resolve("@rivetkit/sqlite/dist/wa-sqlite-async.wasm"); const wasmBinary = readFileSync(wasmPath); - // Initialize wa-sqlite module - each instance gets its own module + // Initialize @rivetkit/sqlite module - each instance gets its own module const module = await SQLiteESMFactory({ wasmBinary }); this.#sqlite3 = Factory(module) as unknown as SQLite3Api; @@ -266,7 +266,7 @@ export class SqliteVfs { // Serialize all open operations within this instance await this.#openMutex.acquire(); try { - // Initialize wa-sqlite and SqliteSystem on first call + // Initialize @rivetkit/sqlite and SqliteSystem on first call await this.#ensureInitialized(); if (!this.#sqlite3 || !this.#sqliteSystem) { diff --git a/website/src/content/cookbook/multiplayer-game.mdx b/website/src/content/cookbook/multiplayer-game.mdx index c82ca5dcae..a3c33c492a 100644 --- a/website/src/content/cookbook/multiplayer-game.mdx +++ b/website/src/content/cookbook/multiplayer-game.mdx @@ -27,106 +27,87 @@ Start with one of the working examples on [GitHub](https://github.com/rivet-dev/ ### Game Loop And Tick Rates -Per-pattern tick rate recommendations are listed in the summary bullets at the start of each architecture pattern. - -#### Implementing Tick Loops - | Pattern | Use When | Implementation Guidance | | --- | --- | --- | | Fixed realtime loop | Battle Royale, Arena, IO Style, Open World, Ranked | Run in `run` with `sleep(tickMs)` and exit on `c.aborted`. | | Action-driven updates | Party, Turn-Based | Mutate and broadcast only on actions/events rather than scheduled ticks. | | Coarse offline progression | Any mode with idle progression | Use `c.schedule.after(...)` with coarse windows (for example 5 to 15 minutes) and apply catch-up from elapsed wall clock time. | -### Physics And Spatial Indexing +### Physics -Per-pattern physics recommendations and optional spatial indexing notes are listed in the summary tables at the start of each architecture pattern. +Start with custom kinematic logic for simple games. Switch to a full physics engine when you need joints, stacked bodies, high collision density, or complex shapes (rotated polygons, capsules, convex hulls, triangle meshes). -| Dimension | Primary Engine | Fallback Engines | Example Code | Notes | -| --- | --- | --- | --- | --- | -| 2D | `@dimforge/rapier2d` | `planck-js`, `matter-js` | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/physics-2d/) | Prefer custom kinematic logic first for simple games, then escalate when contacts become complex. | -| 3D | `@dimforge/rapier3d` | `cannon-es`, `ammo.js` | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/physics-3d/) | For multiplayer shooters, use server-side physics for authoritative combat simulation and movement validation. | +Pick one engine per simulation. Keep frontend-only libs out of backend simulation paths and treat server state as authoritative. -| Optional Spatial Indexing | Recommendation | -| --- | --- | -| Broadphase | Do not use naive `O(n^2)` checks once entity counts can grow. Use a proper broadphase or partitioning structure. | -| AABB index | For non-physics queries (AOI, visibility, non-collider entities), use `rbush` for dynamic sets or `flatbush` for static-ish sets. | -| Point index | For non-physics nearest-neighbor or within-radius queries, use `d3-quadtree`. | +| Dimension | Primary Engine | Fallback Engines | Example Code | +| --- | --- | --- | --- | +| 2D | `@dimforge/rapier2d` | `planck-js`, `matter-js` | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/physics-2d/) | +| 3D | `@dimforge/rapier3d` | `cannon-es`, `ammo.js` | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/physics-3d/) | -| Escalate To Rapier When | Why | -| --- | --- | -| You need joints, stacked bodies, or stable contact manifolds | These need robust rigid-body/contact solvers. | -| You have many dynamic bodies or high collision density | Purpose-built broadphase and solvers hold up better than custom kinematic logic. | -| You need complex shapes (rotated boxes/polygons in 2D, capsules/convex hulls/triangle meshes in 3D) | Hand-rolled collision code becomes costly and error-prone. | +### Spatial Indexing + +For non-physics spatial queries, use a dedicated index instead of naive `O(n^2)` checks: -| Cross-Cutting Rule | Recommendation | +| Index Type | Recommendation | | --- | --- | -| Engine exclusivity | Physics engines are mutually exclusive in practice for one simulation. Pick one engine per simulation. | -| Backend/runtime boundary | Keep frontend-only libs out of backend simulation paths; treat server state as authoritative. | -| Client mesh raycasts | `three-mesh-bvh` is optional and mostly for client-side fast raycasts on dense static meshes. | +| AABB index | For AOI, visibility, and non-collider entities, use `rbush` for dynamic sets or `flatbush` for static-ish sets. | +| Point index | For nearest-neighbor or within-radius queries, use `d3-quadtree`. | ## Networking & State Sync ### Netcode -Per-pattern netcode recommendations live under each architecture pattern in `#### Netcode`. - -| Model | When To Use | How To Implement (Basic) | +| Model | When To Use | Implementation | | --- | --- | --- | -| Hybrid (client movement, server combat) | Latency-sensitive competitive games where movement responsiveness is critical but combat integrity matters (shooters, action sports, ranked duels). | Client owns movement locally and sends position updates at a capped rate. Server validates speed, bounds, and collision for anti-cheat. Projectiles, hits, damage, and other game entities are fully server-authoritative. Server simulates combat from its own state and broadcasts results. | -| Server-authoritative with interpolation | Realtime games where fairness and simplicity outweigh instant local feedback (IO Style, persistent worlds). | Client sends input commands. Server simulates on fixed ticks, resolves movement and collision, and publishes authoritative snapshots. Client interpolates between snapshots and extrapolates briefly on missed updates. | -| Server-authoritative (basic logic) | Event-driven or turn-based games with no realtime simulation. | Server validates and applies discrete actions (turns, phase transitions, votes). No continuous tick loop needed. Client displays confirmed server state. | +| Hybrid (client movement, server combat) | Shooters, action sports, ranked duels | Client owns movement and sends capped-rate position updates. Server validates for anti-cheat. Combat (projectiles, hits, damage) is fully server-authoritative. | +| Server-authoritative with interpolation | IO Style, persistent worlds | Client sends input commands. Server simulates on fixed ticks and publishes authoritative snapshots. Client interpolates between snapshots. | +| Server-authoritative (basic logic) | Turn-based, event-driven | Server validates and applies discrete actions (turns, phase transitions, votes). Client displays confirmed state. | ### Realtime Data Model -- Publish game state from the server as events, using snapshots and/or diffs. In hybrid modes, this covers combat, entities, and other players. In server-authoritative modes, this covers everything including movement. - - Example pattern: send a full snapshot on join/resync, then send per-tick diffs for regular updates. -- Keep events small and typed. For high-frequency updates, batch per tick. -- Avoid using UI-framework state/hooks for high-frequency game updates. Keep simulation and render loops in engine code, and use framework state for basic UI only (menus, HUD, forms), especially with Canvas or Three.js renderers. -- When all players should receive the same update, use `c.broadcast(...)`. When players should receive different/private data, send per-connection payloads with `conn.send(...)`. -- Frontend bundling: place shared types, constants, and pure helpers in `src/shared/` (for example `src/shared/sim/*`). Avoid importing backend actor modules into browser bundles, since backend modules often include Node-only dependencies (`fs`, SQLite bindings, etc). +- **Snapshots and diffs**: Publish state as events. Send a full snapshot on join/resync, then per-tick diffs for regular updates. +- **Batch per tick**: Keep events small and typed. Batch high-frequency updates per tick. +- **Avoid UI framework state for game updates**: Use `requestAnimationFrame` or a Canvas/Three.js loop for simulation, not React state. Reserve UI framework state for menus, HUD, and forms. +- **Broadcast vs per-connection**: Use `c.broadcast(...)` for shared updates and `conn.send(...)` for private/per-player data. ### Shared Simulation Logic -Use this for logic shared between client and server. +Shared simulation logic runs on both the client and the server. For example, an `applyInput(state, input, dt)` function that integrates velocity and clamps to world bounds can run on the client for prediction and on the server for validation. + +- **Hybrid modes**: Client runs shared movement as primary authority, server runs it for anti-cheat validation. +- **Server-authoritative modes**: Client uses shared logic for interpolation and prediction only. +- **Keep it pure**: Movement integration, input transforms, collision helpers, and constants only. +- **Put shared code in `src/shared/`**: Keep deterministic helpers in `src/shared/sim/*` with no side effects. -- Keep shared logic basic and pure (movement integration, input transforms, constants). -- Put shared code in `src/shared/`, and keep deterministic simulation helpers in `src/shared/sim/*` with no side effects. -- Do not share actor runtime code (DB access, network calls, `c.*` context, timers). -- In hybrid modes, the client runs shared movement logic as the primary authority and the server runs it for anti-cheat validation. In server-authoritative modes, the client uses shared logic for interpolation and prediction only. ### Interest Management +Control what each client receives to reduce bandwidth and prevent information leaks. + #### Per-Player Replication Filters -- Send each client only the state relevant to that player (for example by proximity, line-of-sight, team, or game phase). -- For shooters and action games, limit replication by proximity and optional field-of-view checks. -- Keep filtering server-side so clients never receive data they should not see. +- **Filter by relevance**: Send each client only state relevant to that player (proximity, line-of-sight, team, or game phase). +- **Shooters and action games**: Limit replication by proximity and optional field-of-view checks. +- **Server-side only**: Clients should never receive data they should not see. #### Sharded Worlds -- Partition very large worlds across data actors (for example chunk actors keyed by `worldId:chunkX:chunkY`). -- Clients subscribe only to nearby partitions (for example a 3x3 chunk window around the player). -- Use this when you are confident the world is large and state-heavy (for example sandbox builders or MMOs), not as a default for small matches. +- **Partition large worlds**: Use chunk actors keyed by `worldId:chunkX:chunkY`. +- **Subscribe to nearby chunks**: Clients connect only to nearby partitions (for example a 3x3 chunk window). +- **Use sparingly**: Only when the world is large and state-heavy (sandbox builders, MMOs), not as a default for small matches. ## Backend Infrastructure ### Persistence -Use this section to decide when SQLite is a better fit than in-memory actor state. - -- Rivet Actor state is fine for small ephemeral state (rooms, short-lived matches). -- Prefer SQLite (`rivetkit/db`) when state is: - - Large or table-like (tiles/blocks/buildings/inventory). - - Needs queries/indexes beyond key lookups. - - Expected to persist long-term and grow over time. -- Matchmakers/coordinators may also use SQLite when their indexing state can grow large (room registries, matchmaking pools, large queues). -- When using `rivetkit/db`, assume multiple actions can hit the same actor DB concurrently. Serialize DB work through a queue. +- **In-memory state**: Best for realtime game state that changes every tick (player positions, inputs, match phase, scores). +- **SQLite (`rivetkit/db`)**: Better for large or table-like state that needs queries, indexes, or long-term persistence (tiles, inventory, matchmaking pools). Serialize DB work through a queue since multiple actions can hit the same actor concurrently. -### Matchmaking Orchestration Primitives +### Matchmaking Patterns -Use these cross-cutting primitives as a baseline before picking a specific architecture pattern. +Common building blocks used across the architecture patterns below. -#### Actor Topology Primitives +#### Actor Topology | Primitive | Use When | Typical Ownership | | --- | --- | --- | @@ -136,21 +117,10 @@ Use these cross-cutting primitives as a baseline before picking a specific archi | `player[username]` | Canonical profile/rating reused across matches | Durable player stats (for example rating and win/loss). | | `leaderboard["main"]` | Shared rankings across many matches/players | Global ordered score rows and top lists. | -#### Matchmaking Building Blocks - -- **Queue row**: A pending intent to match (mode/rating/capacity metadata). -- **Assignment row**: Binds a connection/player to a specific `matchId`. -- **Pending reservation**: Temporarily holds a seat until a player connects. -- **Join token/invite code**: Server-issued proof that a connect request is allowed. -- **Occupancy heartbeat**: Match reports connected and pending counts back to matchmaker. -- **Close/cleanup flow**: Remove queue, assignment, and reservation rows when a match ends. - #### Queueing Strategy -- Prefer actor queues to serialize DB mutations on shared matchmaking tables. -- Queue matchmaker actions that can race: find/create, queue/unqueue, verify/pending-connected, update/close. -- Keep queueing on the actor that owns the shared index tables (usually the matchmaker). -- Let match-local actions run without queueing unless they mutate shared matchmaking indexes. +- Multiple players can hit the matchmaker at the same time, so actions like find/create, queue/unqueue, and close need to be serialized through actor queues to avoid races. +- Match-local actions (gameplay, scoring) do not need queueing unless they write back to the matchmaker. ## Security And Anti-Cheat @@ -158,27 +128,15 @@ Start with this baseline, then harden further for competitive or high-risk envir ### Baseline Checklist -Apply this checklist to all implementations. - -- Identity: - - Use `c.conn.id` as the authoritative transport identity of the caller. - - If you accept `playerId`/`username` in params, treat it as untrusted input and bind it through server-issued assignment/join tickets before attaching player state. -- Authorization: - - Validate that the caller is allowed to mutate the target entity (room membership, turn ownership, host-only actions). -- Input validation: - - Clamp sizes/lengths and validate enums. - - Validate usernames (length, allowed chars, avoid unbounded Unicode). -- Rate limiting: - - Per-connection rate limits for spammy actions (chat, join/leave, fire, movement updates). -- State integrity: - - Server recomputes derived state (scores, win conditions, placements). - - Avoid client-authoritative changes to inventory/currency/leaderboard totals. +- **Identity**: Use `c.conn.id` as the authoritative transport identity. Treat `playerId`/`username` in params as untrusted input and bind through server-issued assignment/join tickets. +- **Authorization**: Validate the caller is allowed to mutate the target entity (room membership, turn ownership, host-only actions). +- **Input validation**: Clamp sizes/lengths, validate enums, and validate usernames (length, allowed chars, avoid unbounded Unicode). +- **Rate limiting**: Per-connection rate limits for spammy actions (chat, join/leave, fire, movement updates). +- **State integrity**: Server recomputes derived state (scores, win conditions, placements). Never allow client-authoritative changes to inventory/currency/leaderboard totals. ### Movement Validation -Use this for any mode with client-authoritative movement (hybrid and client-authoritative flows). - -Clients may send position/rotation updates for smoothness, but the server must: +For any mode with client-authoritative movement (hybrid flows), clients may send position/rotation updates for smoothness, but the server must: - Enforce max delta per update (speed cap) based on elapsed time. - Reject or clamp teleports. diff --git a/website/src/pages/cookbook/[...slug].astro b/website/src/pages/cookbook/[...slug].astro index a0ff472163..70355898a4 100644 --- a/website/src/pages/cookbook/[...slug].astro +++ b/website/src/pages/cookbook/[...slug].astro @@ -125,7 +125,6 @@ const resolvedTemplates = resolveTemplates(templates);

Install skill for coding agents

-

Let your AI coding agent follow this guide automatically.

npx skills add rivet-dev/skills -s {skillId}
-

Supports Claude Code, Cursor, Windsurf, and other AI coding assistants.

+

Supports Claude Code, Codex, Cursor, and other AI coding assistants.