diff --git a/vendor/actors/docs/CLAUDE.md b/vendor/actors/docs/CLAUDE.md index 3d3509d..2d6ae04 100644 --- a/vendor/actors/docs/CLAUDE.md +++ b/vendor/actors/docs/CLAUDE.md @@ -77,7 +77,7 @@ the website's icon package. - **Marketing pages.** They live in the website repo. - **Deploy and self-hosting guides.** They are written once in the website repo - and templated across all four products. Do not write a per-product copy. + and templated across every product. Do not write a per-product copy. - **Website components.** Do not import from the website by relative path or alias; a page must render from the components the site already provides. diff --git a/vendor/actors/docs/content/docs/actions.mdx b/vendor/actors/docs/content/docs/actions.mdx index c0a15a1..049ad24 100644 --- a/vendor/actors/docs/content/docs/actions.mdx +++ b/vendor/actors/docs/content/docs/actions.mdx @@ -133,11 +133,3 @@ See [types](/actors/docs/types) for more details on using `ActionContextOf` and - `GET /inspector/rpcs` lists all available actions on an actor. - `POST /inspector/action/:name` executes an action with JSON args and returns output. - In non-dev mode, inspector endpoints require authorization. - -## API Reference - -- [`Actions`](/typedoc/interfaces/rivetkit.mod.Actions.html) - Interface for defining actions -- [`ActionContext`](/typedoc/interfaces/rivetkit.mod.ActionContext.html) - Context available in action handlers -- [`ActorDefinition`](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - Interface for defining actors with actions -- [`ActorHandle`](/typedoc/types/rivetkit.client_mod.ActorHandle.html) - Handle for calling actions from client -- [`ActorActionFunction`](/typedoc/types/rivetkit.client_mod.ActorActionFunction.html) - Type for action functions diff --git a/vendor/actors/docs/content/docs/authentication.mdx b/vendor/actors/docs/content/docs/authentication.mdx index 5f20d64..1ff9048 100644 --- a/vendor/actors/docs/content/docs/authentication.mdx +++ b/vendor/actors/docs/content/docs/authentication.mdx @@ -122,9 +122,3 @@ The limits in this example are [ephemeral](/actors/docs/state#ephemeral-variable Cache validated tokens in `c.vars` to avoid redundant validation on repeated connections. See [ephemeral variables](/actors/docs/state#ephemeral-variables) for more details. - -## API Reference - -- [`AuthIntent`](/typedoc/types/rivetkit.mod.AuthIntent.html) - Authentication intent type -- [`OnBeforeConnectContext`](/typedoc/interfaces/rivetkit.mod.OnBeforeConnectContext.html) - Context for auth checks -- [`OnConnectContext`](/typedoc/interfaces/rivetkit.mod.OnConnectContext.html) - Context after connection diff --git a/vendor/actors/docs/content/docs/clients/javascript.mdx b/vendor/actors/docs/content/docs/clients/javascript.mdx index ce8f6dc..d3bfccf 100644 --- a/vendor/actors/docs/content/docs/clients/javascript.mdx +++ b/vendor/actors/docs/content/docs/clients/javascript.mdx @@ -139,6 +139,3 @@ Requests can still return transient lifecycle or gateway errors. Retry once the **Package:** [rivetkit](https://www.npmjs.com/package/rivetkit) See the [RivetKit client overview](/actors/docs/clients). - -- [`createClient`](/typedoc/functions/rivetkit.client_mod.createClient.html) - Create a client -- [`Client`](/typedoc/types/rivetkit.mod.Client.html) - Client type diff --git a/vendor/actors/docs/content/docs/communicating-between-actors.mdx b/vendor/actors/docs/content/docs/communicating-between-actors.mdx index cf46f40..fb1cb37 100644 --- a/vendor/actors/docs/content/docs/communicating-between-actors.mdx +++ b/vendor/actors/docs/content/docs/communicating-between-actors.mdx @@ -45,9 +45,3 @@ Use connections to listen for events from other actors: Process multiple items in parallel: - -## API Reference - -- [`ActorHandle`](/typedoc/types/rivetkit.client_mod.ActorHandle.html) - Handle for calling other actors -- [`Client`](/typedoc/types/rivetkit.mod.Client.html) - Client type for actor communication -- [`ActorAccessor`](/typedoc/interfaces/rivetkit.client_mod.ActorAccessor.html) - Accessor for getting actor handles diff --git a/vendor/actors/docs/content/docs/connections.mdx b/vendor/actors/docs/content/docs/connections.mdx index 5a92fd7..2f7ad4e 100644 --- a/vendor/actors/docs/content/docs/connections.mdx +++ b/vendor/actors/docs/content/docs/connections.mdx @@ -58,8 +58,6 @@ Pending connections are not visible in `c.conns` while `onBeforeConnect` or `cre ### `createConnState` and `connState` -[API Reference](/typedoc/interfaces/rivetkit.mod.CreateConnStateContext.html) - There are two ways to define the initial state for connections: 1. `connState`: Define a constant object that will be used as the initial state for all connections 2. `createConnState`: A function that dynamically creates initial connection state based on connection parameters. Can be async. @@ -68,8 +66,6 @@ Connections are not visible in `c.conns` until `createConnState` completes succe ### `onBeforeConnect` -[API Reference](/typedoc/interfaces/rivetkit.mod.OnBeforeConnectContext.html) - The `onBeforeConnect` hook is called whenever a new client connects to the actor. Can be async. Clients can pass parameters when connecting, accessible via `params`. This hook is used for connection validation and can throw errors to reject connections. The `onBeforeConnect` hook does NOT return connection state - it's used solely for validation. @@ -82,8 +78,6 @@ Connections cannot interact with the actor until this method completes successfu ### `onConnect` -[API Reference](/typedoc/interfaces/rivetkit.mod.OnConnectContext.html) - Executed after the client has successfully connected. Can be async. Receives the connection object as a second parameter. By the time `onConnect` runs, the connection is visible in `c.conns`. @@ -94,8 +88,6 @@ Messages will not be processed for this actor until this hook succeeds. Errors t ### `onDisconnect` -[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - Called when a client disconnects from the actor. Can be async. Receives the connection object as a second parameter. Use this to clean up any connection-specific resources. @@ -125,12 +117,3 @@ If you need to wait for the disconnection to complete, you can use `await`: This ensures the underlying network connections close cleanly before continuing. - -## API Reference - -- [`Conn`](/typedoc/interfaces/rivetkit.mod.Conn.html) - Connection interface -- [`ConnInitContext`](/typedoc/interfaces/rivetkit.mod.ConnInitContext.html) - Connection initialization context -- [`CreateConnStateContext`](/typedoc/interfaces/rivetkit.mod.CreateConnStateContext.html) - Context for creating connection state -- [`OnBeforeConnectContext`](/typedoc/interfaces/rivetkit.mod.OnBeforeConnectContext.html) - Pre-connection lifecycle hook context -- [`OnConnectContext`](/typedoc/interfaces/rivetkit.mod.OnConnectContext.html) - Post-connection lifecycle hook context -- [`ActorConn`](/typedoc/types/rivetkit.client_mod.ActorConn.html) - Typed connection from client side diff --git a/vendor/actors/docs/content/docs/container-runner.mdx b/vendor/actors/docs/content/docs/container-runner.mdx new file mode 100644 index 0000000..c9ff068 --- /dev/null +++ b/vendor/actors/docs/content/docs/container-runner.mdx @@ -0,0 +1,100 @@ +--- +title: "Container Runner" +description: "Run any containerized server as a Rivet Actor." +skill: true +--- + +The container runner (`rivet-container-runner`) is an adapter for running arbitrary containers as Rivet Actors. Use it for non-RivetKit workloads such as Unity or Godot dedicated game servers and batch jobs like FFmpeg transcoding. + +## Steps + + + + +- A containerized server (a Unity or Godot dedicated server, a plain Node process, or any HTTP/WebSocket server) +- Access to the [Rivet Cloud](https://dashboard.rivet.dev/) or a [self-hosted Rivet Engine](/actors/self-host) +- Docker running locally + + + + +Download the static binary from Rivet's release artifacts in your Dockerfile and set it as the entrypoint, passing your server's launch command after `--`: + +```dockerfile @nocheck +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/* + +# Install the Rivet container runner. +RUN curl -fsSL https://releases.rivet.dev/rivet/latest/container-runner/rivet-container-runner-x86_64-unknown-linux-musl \ + -o /usr/local/bin/rivet-container-runner \ + && chmod +x /usr/local/bin/rivet-container-runner + +# Your server binary and assets. +COPY build/ /game/ +WORKDIR /game + +ENTRYPOINT ["rivet-container-runner", "--", "./GameServer", "-batchmode", "-nographics", "-logFile", "-"] +``` + +Artifacts are published for `x86_64-unknown-linux-musl` and `aarch64-unknown-linux-musl`. The binaries are fully static, so they run in any Linux base image, including `scratch`. Pin a version by replacing `latest` with a release version, for example `https://releases.rivet.dev/rivet/2.3.3/container-runner/rivet-container-runner-x86_64-unknown-linux-musl`. + + + + +Deploy the image to Rivet Compute with the CLI. For game servers, configure the pool with one actor per instance and keep running instances alive across version upgrades: + +```bash +npx @rivetkit/cli deploy \ + --token "$RIVET_CLOUD_TOKEN" \ + --instance-request-concurrency 1 \ + --drain-on-version-upgrade false \ + --dockerfile Dockerfile +``` + + + + +Create actors against the pool's runner (`default`) and connect clients through the gateway URL shown in the dashboard. WebSocket clients connect at the bare gateway URL with the `rivet` WebSocket subprotocol. + + + + +## How It Works + +1. The engine cold-starts your container and calls `POST /api/rivet/start` on the port it injects as `RIVET_PORT`. +2. The runner spawns your server as a child process with `PORT` set to the child port, waits for the port to open, and reports the actor as running. +3. Gateway traffic for the actor arrives over Rivet's tunnel and is proxied to `127.0.0.1:`. WebSocket clients connect at the bare gateway URL with the `rivet` WebSocket subprotocol. Raw HTTP reaches the child under the `/request/*` prefix on the actor surface (the prefix is stripped before proxying); other paths are reserved for the runtime's own endpoints. +4. Child stdout and stderr are re-emitted with an `[actorId=... key=...]` prefix so actor logs are attributed in the dashboard. +5. When an actor stops, the runner sends its child `SIGTERM`, escalates to `SIGKILL` after a grace period, and exits the process once no actors remain. + +## Configuration + +All flags can also be set through environment variables: + +| Flag | Environment variable | Default | Description | +| --- | --- | --- | --- | +| `--port` | `RIVET_PORT` / `PORT` | `8080` | Serverless front-door HTTP port. Rivet Compute injects `RIVET_PORT` automatically. | +| `--child-port` | `CHILD_PORT` | `7770` | First local child port; each actor's child gets the next free port at or above this, exported to the child as `PORT`. | +| `--actor-name` | `RIVET_ACTOR_NAME` | `game` | Actor name this runner serves. | +| `--runner-version` | `RIVET_RUNNER_VERSION` | `1` | Version reported to the engine, used to drain old runners on deploy. | +| `--base-path` | `RIVET_SERVERLESS_BASE_PATH` | `/api/rivet` | Base path the engine calls for serverless start. | +| `--stop-grace-secs` | `RIVET_STOP_GRACE_SECS` | `25` | `SIGTERM` to `SIGKILL` grace period when stopping the child. Capped to a few seconds when the platform itself is reclaiming the instance, so shutdown fits inside the platform's own kill window. | +| `--readiness-timeout-secs` | `RIVET_READINESS_TIMEOUT_SECS` | `30` | How long to wait for the child's port to open before failing the start. | + +### Per-Actor Input + +The actor's `input` payload can override the launch spec per actor. All fields are optional and fall back to the entrypoint command. RivetKit clients pass this object directly; when creating actors through the raw engine API, encode it as CBOR before base64-encoding the `input` field: + +```json +{ + "command": ["./GameServer", "-batchmode"], + "args": ["-extra-flag"], + "env": { "MATCH_MODE": "ranked" } +} +``` + +`command` replaces the entrypoint command template, `args` are appended to it, and `env` adds environment variables for the child. + +## Source and Examples + +The runner and a full end-to-end example, including a Unity FishNet demo project and a local test harness, live in the Rivet repository under [`container-runner/`](https://github.com/rivet-dev/rivet/tree/main/container-runner). diff --git a/vendor/actors/docs/content/docs/design-patterns.mdx b/vendor/actors/docs/content/docs/design-patterns.mdx index 8bf8073..d0b4e34 100644 --- a/vendor/actors/docs/content/docs/design-patterns.mdx +++ b/vendor/actors/docs/content/docs/design-patterns.mdx @@ -146,7 +146,6 @@ Use this when: - ### Syncing State Changes Use `onStateChange` to automatically sync actor state changes to external resources. This hook runs after state changes are flushed, which is coalesced to once per event loop tick rather than once per individual field mutation. @@ -208,9 +207,3 @@ Actors are designed to maintain state across multiple requests. Creating a new a **Solution:** Use actors for entities that persist (users, sessions, documents), not for one-off operations. For stateless request handling, use regular functions. - -## API Reference - -- [`ActorDefinition`](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - Interface for pattern examples -- [`ActorContext`](/typedoc/interfaces/rivetkit.mod.ActorContext.html) - Context usage patterns -- [`ActionContext`](/typedoc/interfaces/rivetkit.mod.ActionContext.html) - Action patterns diff --git a/vendor/actors/docs/content/docs/destroy.mdx b/vendor/actors/docs/content/docs/destroy.mdx index 1e529fc..5dea864 100644 --- a/vendor/actors/docs/content/docs/destroy.mdx +++ b/vendor/actors/docs/content/docs/destroy.mdx @@ -68,8 +68,3 @@ Once destroyed, the `onDestroy` hook will be called. This can be used to clean u ## Accessing Actor After Destroy Once an actor is destroyed, any subsequent requests to it will fail with an `actor.not_found` error (`{ group: "actor", code: "not_found" }`). The actor's state is permanently deleted. - -## API Reference - -- [`ActorHandle`](/typedoc/types/rivetkit.client_mod.ActorHandle.html) - Has destroy methods -- [`ActorContext`](/typedoc/interfaces/rivetkit.mod.ActorContext.html) - Context during destruction diff --git a/vendor/actors/docs/content/docs/errors.mdx b/vendor/actors/docs/content/docs/errors.mdx index 2b309a9..c1fbdd0 100644 --- a/vendor/actors/docs/content/docs/errors.mdx +++ b/vendor/actors/docs/content/docs/errors.mdx @@ -121,9 +121,3 @@ For faster debugging during development, you can expose internal error details t With error exposure enabled, clients will see the full error message instead of the generic "Internal error" response: - -## API Reference - -- [`UserError`](/typedoc/classes/rivetkit.actor_errors.UserError.html) - User-facing error class -- [`ActorError`](/typedoc/classes/rivetkit.client_mod.ActorError.html) - Errors received by the client - diff --git a/vendor/actors/docs/content/docs/events.mdx b/vendor/actors/docs/content/docs/events.mdx index 6d24788..1072c4e 100644 --- a/vendor/actors/docs/content/docs/events.mdx +++ b/vendor/actors/docs/content/docs/events.mdx @@ -152,13 +152,3 @@ function ConditionalListener() { ## More About Connections For more details on actor connections, including connection lifecycle, authentication, and advanced connection patterns, see the [Connections documentation](/actors/docs/connections). - -## API Reference - -- [`RivetEvent`](/typedoc/interfaces/rivetkit.mod.RivetEvent.html) - Base event interface -- [`RivetMessageEvent`](/typedoc/interfaces/rivetkit.mod.RivetMessageEvent.html) - Message event type -- [`RivetCloseEvent`](/typedoc/interfaces/rivetkit.mod.RivetCloseEvent.html) - Close event type -- [`UniversalEvent`](/typedoc/interfaces/rivetkit.mod.UniversalEvent.html) - Universal event type -- [`UniversalMessageEvent`](/typedoc/interfaces/rivetkit.mod.UniversalMessageEvent.html) - Universal message event -- [`UniversalErrorEvent`](/typedoc/interfaces/rivetkit.mod.UniversalErrorEvent.html) - Universal error event -- [`EventUnsubscribe`](/typedoc/types/rivetkit.client_mod.EventUnsubscribe.html) - Unsubscribe function type diff --git a/vendor/actors/docs/content/docs/general/docs-for-llms.mdx b/vendor/actors/docs/content/docs/general/docs-for-llms.mdx deleted file mode 100644 index d9d6c02..0000000 --- a/vendor/actors/docs/content/docs/general/docs-for-llms.mdx +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: "Documentation for LLMs & AI" -description: "Rivet provides optimized documentation formats specifically designed for Large Language Models (LLMs) and AI integration tools." -skill: true ---- - -## Skills (Recommended) - -For AI coding assistants like Claude Code, Cursor, or Windsurf, install Rivet skills for the best development experience: - -```sh -npx skills add rivet-dev/skills -``` - -Skills provide your AI assistant with Rivet-specific knowledge, best practices, and code patterns directly in your project context. - -## Available Formats - -### `llms.txt` (Condensed) -A condensed version of the documentation perfect for quick reference and context-aware AI assistance. - -**Access:** /llms.txt - -This format includes: -- Key concepts and features -- Essential getting started information -- Summaries of main functionality -- Optimized for token efficiency - -### `llms-full.txt` (Complete) -The complete documentation in a single file, ideal for comprehensive AI assistance and in-depth analysis. - -**Access:** /llms-full.txt - -This format includes: -- Complete documentation content -- All examples and detailed explanations -- Full API references and guides -- Suitable for complex queries and comprehensive understanding - -## Access Pages As Markdown - -Each documentation page is also available as clean markdown by appending `.md` to any documentation URL path. - -For example: - -- Original URL: `https://rivet.dev/actors/docs` -- Markdown URL: `https://rivet.dev/actors/docs.md` - diff --git a/vendor/actors/docs/content/docs/general/edge.mdx b/vendor/actors/docs/content/docs/general/edge.mdx index 30f8588..4c80133 100644 --- a/vendor/actors/docs/content/docs/general/edge.mdx +++ b/vendor/actors/docs/content/docs/general/edge.mdx @@ -1,26 +1,37 @@ --- -title: "Edge Networking" -description: "Actors automatically run near your users on your provider's global network." +title: "Regions & Multi-Region" +description: "Actors run near your users, in a region you can choose." skill: true --- - - At the moment, edge networking is only supported on Rivet Cloud & Cloudflare Workers. More self-hosted platforms are on the roadmap. - +An actor lives in one region. Which region it lands in, and how a client reaches it, is the same model whether you run on Rivet Cloud or your own multi-region deployment. ## Region selection -### Automatic region selection - -By default, actors will choose the nearest region based on the client's location. +### Automatic -Under the hood, Rivet and Cloudflare use [Anycast routing](https://en.wikipedia.org/wiki/Anycast) to automatically find the best location for the client to connect to without relying on a slow manual pinging process. +By default, an actor is created in the region nearest the client. Rivet uses [Anycast routing](https://en.wikipedia.org/wiki/Anycast) to find the closest point of presence without a slow manual pinging round. -### Manual region selection +### Manual -The region an actor is created in can be overridden using region options: +Override the region with region options at create time: -See [Create Manage Actors](/actors/docs/communicating-between-actors) for more information. +See [Actor-Actor Communication](/actors/docs/communicating-between-actors) for the full set of create options. + +## Where the actor stays + +An actor does not migrate between regions. It is created in one region and stays there for its lifetime, so its state is always local to the compute running it. That locality is the point: reads and writes never cross a region boundary. + +Communication between actors in different regions goes over the network, so treat a cross-region actor call the same way you would treat any other remote call. + +## Multi-region when self-hosting + + +Edge networking with automatic region selection is available on Rivet Cloud and Cloudflare Workers. Self-hosted deployments can run multiple regions, but you configure the topology and hostnames yourself. + + +A self-hosted multi-region deployment runs a control plane in each region, all sharing one database and pub/sub layer, with each region reachable at its own hostname. See [Multi-Region](/actors/self-host/control-plane/multi-region) in the self-host docs for the topology configuration. +The rule that matters most: each region needs its **own** hostname. Pointing a shared, load-balanced origin at several regions makes it impossible to address a specific one. diff --git a/vendor/actors/docs/content/docs/input.mdx b/vendor/actors/docs/content/docs/input.mdx index 3796333..165ff6b 100644 --- a/vendor/actors/docs/content/docs/input.mdx +++ b/vendor/actors/docs/content/docs/input.mdx @@ -52,9 +52,3 @@ Define input types to ensure type safety: Input is only available in `createState` and `onCreate` lifecycle hooks. If you need to access input data later (in actions, timers, or other hooks), store it in the actor's state during creation. This is the recommended pattern because input shapes can evolve over time, and persisting input in state ensures you always have access to the values the actor was created with: - -## API Reference - -- [`CreateOptions`](/typedoc/interfaces/rivetkit.client_mod.CreateOptions.html) - Options for creating actors -- [`CreateRequest`](/typedoc/types/rivetkit.client_mod.CreateRequest.html) - Request type for creation -- [`ActorDefinition`](/typedoc/classes/rivetkit.mod.ActorDefinition.html) - Actor definition returned by `actor()` diff --git a/vendor/actors/docs/content/docs/keys.mdx b/vendor/actors/docs/content/docs/keys.mdx index 17b538a..6d00bb4 100644 --- a/vendor/actors/docs/content/docs/keys.mdx +++ b/vendor/actors/docs/content/docs/keys.mdx @@ -67,11 +67,3 @@ Use keys to provide basic actor configuration: For more complex configuration, use [input parameters](/actors/docs/input): - -## API Reference - -- [`ActorKey`](/typedoc/types/rivetkit.mod.ActorKey.html) - Key type for actors -- [`ActorQuery`](/typedoc/types/rivetkit.mod.ActorQuery.html) - Query type using keys -- [`GetOptions`](/typedoc/interfaces/rivetkit.client_mod.GetOptions.html) - Options for getting by key -- [`QueryOptions`](/typedoc/interfaces/rivetkit.client_mod.QueryOptions.html) - Options for querying - diff --git a/vendor/actors/docs/content/docs/kv.mdx b/vendor/actors/docs/content/docs/kv.mdx index 18dbaca..6edadd8 100644 --- a/vendor/actors/docs/content/docs/kv.mdx +++ b/vendor/actors/docs/content/docs/kv.mdx @@ -49,7 +49,3 @@ Use `listRange(start, end)` to read an arbitrary half-open range `[start, end)`. KV supports batch operations for efficiency. `batchPut` and `batchGet` work on raw `Uint8Array` keys and values, so encode strings before passing them in. - -## API Reference - -- [`ActorContext`](/typedoc/interfaces/rivetkit.mod.ActorContext.html) - `c.kv` is available on the context diff --git a/vendor/actors/docs/content/docs/lifecycle.mdx b/vendor/actors/docs/content/docs/lifecycle.mdx index fd70a19..3ab6276 100644 --- a/vendor/actors/docs/content/docs/lifecycle.mdx +++ b/vendor/actors/docs/content/docs/lifecycle.mdx @@ -88,16 +88,12 @@ The `state` constant defines the initial state of the actor. See [state document ### `onMigrate` -[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - The `onMigrate` hook runs on every actor start, before `createState`, `onCreate`, `createVars`, and `onWake`. Can be async. It runs early so that database migrations are applied before any other lifecycle hook accesses the database. The second parameter is `true` when the actor is being created for the first time. ### `createState` -[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - The `createState` function dynamically initializes state based on input. Called only once when the actor is first created. Can be async. See [state documentation](/actors/docs/state) for more information. @@ -110,24 +106,18 @@ The `vars` constant defines ephemeral variables for the actor. These variables a ### `createVars` -[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - The `createVars` function dynamically initializes ephemeral variables. Can be async. Use this when you need to initialize values at runtime. See [ephemeral variables documentation](/actors/docs/state#ephemeral-variables) for more information. ### `onCreate` -[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - The `onCreate` hook is called when the actor is first created. Can be async. Use this hook for initialization logic that doesn't affect the initial state. ### `onDestroy` -[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - The `onDestroy` hook is called when the actor is being permanently destroyed. Can be async. Use this for final cleanup operations like closing external connections, releasing resources, or performing any last-minute state persistence. The actor is still fully functional when `onDestroy` runs. You can access the database, broadcast events, call `waitUntil`, send queue messages, and use `schedule.after`. State mutations made during `onDestroy` are persisted before the actor is torn down. @@ -136,8 +126,6 @@ The actor is still fully functional when `onDestroy` runs. You can access the da ### `onWake` -[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - This hook is called any time the actor is started (e.g. after restarting, upgrading code, or crashing). Can be async. This is called after the actor has been initialized but before any connections are accepted. @@ -148,8 +136,6 @@ Use this hook to set up any resources or start any background tasks, such as `se ### `onSleep` -[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - This hook is called when the actor is going to sleep. Can be async. Use this to clean up resources, close connections, or perform any shutdown operations. The actor is still fully functional when `onSleep` runs. You can access the database, broadcast events, call `waitUntil`, send queue messages, and use `schedule.after`. State mutations made during `onSleep` are persisted before the actor finishes sleeping. @@ -162,8 +148,6 @@ Not supported on Cloudflare Workers. ### `run` -[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - The `run` hook is called after the actor starts and runs in the background without blocking actor startup. This is ideal for long-running background tasks like: - Reading from message queues in a loop @@ -189,8 +173,6 @@ Finite `run` handlers leave the actor alive after they finish. If you want a one ### `onStateChange` -[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - Called whenever the actor's state changes. Cannot be async. This is often used to broadcast state updates. Do not mutate `c.state` inside `onStateChange`; re-entrant state mutation is rejected. @@ -199,16 +181,12 @@ Do not mutate `c.state` inside `onStateChange`; re-entrant state mutation is rej ### `createConnState` and `connState` -[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - There are two ways to define the initial state for connections: 1. `connState`: Define a constant object that will be used as the initial state for all connections 2. `createConnState`: A function that dynamically creates initial connection state based on connection parameters. Can be async. ### `onBeforeConnect` -[API Reference](/typedoc/interfaces/rivetkit.mod.BeforeConnectContext.html) - The `onBeforeConnect` hook is called whenever a new client connects to the actor. Can be async. Clients can pass parameters when connecting, accessible via `params`. This hook is used for connection validation and can throw errors to reject connections. The `onBeforeConnect` hook does NOT return connection state - it's used solely for validation. @@ -219,8 +197,6 @@ Connections cannot interact with the actor until this method completes successfu ### `onConnect` -[API Reference](/typedoc/interfaces/rivetkit.mod.ConnectContext.html) - Executed after the client has successfully connected. Can be async. Receives the connection object as a second parameter. @@ -229,8 +205,6 @@ Messages will not be processed for this actor until this hook succeeds. Errors t ### `canPublish` and `canSubscribe` -[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - Use schema-level hooks to authorize queue publishes and event subscriptions. Both hooks can be async and must return booleans: - `queues..canPublish` runs before inbound queue publishes. @@ -244,16 +218,12 @@ Use deny-by-default rules for each hook and return `false` unless explicitly all ### `onDisconnect` -[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - Called when a client disconnects from the actor. Can be async. Receives the connection object as a second parameter. Use this to clean up any connection-specific resources. ### `onRequest` -[API Reference](/typedoc/interfaces/rivetkit.mod.RequestContext.html) - The `onRequest` hook handles HTTP requests sent to your actor at `/actors/{actorName}/http/*` endpoints. Can be async. It receives the request context and a standard `Request` object, and should return a `Response` object. See [Request Handler](/actors/docs/request-handler) for more details. @@ -262,8 +232,6 @@ See [Request Handler](/actors/docs/request-handler) for more details. ### `onWebSocket` -[API Reference](/typedoc/interfaces/rivetkit.mod.WebSocketContext.html) - The `onWebSocket` hook handles WebSocket connections to your actor. Can be async. It receives the actor context and a `WebSocket` object. Use this to set up WebSocket event listeners and handle real-time communication. See [WebSocket Handler](/actors/docs/websocket-handler) for more details. @@ -272,8 +240,6 @@ See [WebSocket Handler](/actors/docs/websocket-handler) for more details. ### `onBeforeActionResponse` -[API Reference](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - The `onBeforeActionResponse` hook is called before sending an action response to the client. Can be async. Use this hook to modify or transform the output of an action before it's sent to the client. This is useful for formatting responses, adding metadata, or applying transformations to the output. diff --git a/vendor/actors/docs/content/docs/limits.mdx b/vendor/actors/docs/content/docs/limits.mdx index a21f314..c783938 100644 --- a/vendor/actors/docs/content/docs/limits.mdx +++ b/vendor/actors/docs/content/docs/limits.mdx @@ -169,4 +169,4 @@ These timeouts control how actors are shut down when a serverless request reache ## Increasing Limits -These limits are sane defaults designed to protect your application from exploits and accidental runaway bugs. If you have a use case that requires different limits, [contact us](https://rivet.dev/contact) to discuss your requirements. +These limits are sane defaults designed to protect your application from exploits and accidental runaway bugs. If you have a use case that requires different limits, [contact us](https://rivet.dev/sales) to discuss your requirements. diff --git a/vendor/actors/docs/content/docs/metadata.mdx b/vendor/actors/docs/content/docs/metadata.mdx index 32709cb..342c84b 100644 --- a/vendor/actors/docs/content/docs/metadata.mdx +++ b/vendor/actors/docs/content/docs/metadata.mdx @@ -45,8 +45,3 @@ Region can be accessed from the context object via `c.region`. - -## API Reference - -- [`ActorDefinition`](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - Interface for defining metadata -- [`CreateOptions`](/typedoc/interfaces/rivetkit.client_mod.CreateOptions.html) - Options for creating an actor, including `region` and `input` diff --git a/vendor/actors/docs/content/docs/quickstart/backend.mdx b/vendor/actors/docs/content/docs/quickstart/backend.mdx index 8f97775..fb3c2d1 100644 --- a/vendor/actors/docs/content/docs/quickstart/backend.mdx +++ b/vendor/actors/docs/content/docs/quickstart/backend.mdx @@ -96,7 +96,7 @@ See the [React documentation](/actors/docs/clients/react) for more information. - + diff --git a/vendor/actors/docs/content/docs/request-handler.mdx b/vendor/actors/docs/content/docs/request-handler.mdx index 43100b7..91c9796 100644 --- a/vendor/actors/docs/content/docs/request-handler.mdx +++ b/vendor/actors/docs/content/docs/request-handler.mdx @@ -94,8 +94,3 @@ The `onRequest` handler is WinterTC compliant and will work with existing librar ### Skip Ready Wait Requests are normally held at the gateway until the actor is ready. Pass `skipReadyWait: true` on `handle.fetch()` to deliver immediately, including while the actor is still starting or in the [sleep grace period](/actors/docs/lifecycle#shutdown-sequence). See [Skip Ready Wait](/actors/docs/clients/javascript#skip-ready-wait) for details. - -## API Reference - -- [`RequestContext`](/typedoc/interfaces/rivetkit.mod.RequestContext.html) - Context for HTTP request handlers -- [`ActorDefinition`](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - Interface for defining request handlers diff --git a/vendor/actors/docs/content/docs/state.mdx b/vendor/actors/docs/content/docs/state.mdx index 0e5fca8..983ef01 100644 --- a/vendor/actors/docs/content/docs/state.mdx +++ b/vendor/actors/docs/content/docs/state.mdx @@ -291,9 +291,3 @@ For the full query API, schema migrations, transactions, and the Drizzle ORM, se - `GET /inspector/state` returns the actor's current state and `isStateEnabled`. - `PATCH /inspector/state` lets you set state directly while debugging. - In non-dev mode, inspector endpoints require authorization. - -## API Reference - -- [`CreateContext`](/typedoc/types/rivetkit.mod.CreateContext.html) - Context available during actor state creation -- [`ActorContext`](/typedoc/interfaces/rivetkit.mod.ActorContext.html) - Context available throughout actor lifecycle -- [`ActorDefinition`](/typedoc/interfaces/rivetkit.mod.ActorDefinition.html) - Interface for defining actors with state diff --git a/vendor/actors/docs/content/docs/testing.mdx b/vendor/actors/docs/content/docs/testing.mdx index eba793b..d41cc3d 100644 --- a/vendor/actors/docs/content/docs/testing.mdx +++ b/vendor/actors/docs/content/docs/testing.mdx @@ -50,7 +50,3 @@ Use a short schedule and `expect.poll` the action's observable result. Vitest's 4. **Use realistic data**: Test with data that resembles production scenarios. `setupTest` starts the registry and disposes the returned client when the test finishes, so you can focus on writing effective tests for your business logic. - -## API Reference - -- [`setupTest`](/typedoc/functions/rivetkit.test_mod.setupTest.html) - Test setup helper function diff --git a/vendor/actors/docs/content/docs/websocket-handler.mdx b/vendor/actors/docs/content/docs/websocket-handler.mdx index 27de32c..8d29732 100644 --- a/vendor/actors/docs/content/docs/websocket-handler.mdx +++ b/vendor/actors/docs/content/docs/websocket-handler.mdx @@ -213,10 +213,3 @@ Connections are normally held at the gateway until the actor is ready. Pass `ski The `onWebSocket` handler can be async, allowing you to perform async code before setting up event listeners: - -## API Reference - -- [`WebSocketContext`](/typedoc/interfaces/rivetkit.mod.WebSocketContext.html) - Context for WebSocket handlers -- [`UniversalWebSocket`](/typedoc/interfaces/rivetkit.mod.UniversalWebSocket.html) - Universal WebSocket interface -- [`handleRawWebSocketHandler`](/typedoc/functions/rivetkit.mod.handleRawWebSocketHandler.html) - Function to handle raw WebSocket -- [`UpgradeWebSocketArgs`](/typedoc/interfaces/rivetkit.mod.UpgradeWebSocketArgs.html) - Arguments for WebSocket upgrade diff --git a/vendor/actors/docs/content/integrations/flue.mdx b/vendor/actors/docs/content/integrations/flue.mdx new file mode 100644 index 0000000..4e92a2b --- /dev/null +++ b/vendor/actors/docs/content/integrations/flue.mdx @@ -0,0 +1,170 @@ +--- +title: "Flue" +description: "Run Flue agents on Rivet with agentOS sandboxes." +skill: false +--- + +import { Hosting } from "@/components/docs/Hosting"; + +{/* + Keep this guide synchronized with + rivet-dev/agentos/website/src/content/docs/docs/frameworks/flue.mdx. + This intentionally hardcodes the agentOS example instead of using CodeSnippet: + importing snippets across repositories would add an agentOS dependency to Rivet. +*/} + + +This integration is in beta. APIs may change between releases. + + +Flue owns the agent runtime and session lifecycle. Rivet maps each agent instance and workflow run to a durable Rivet Actor, while agentOS gives each Flue context an isolated VM with a persistent `/workspace` filesystem. + + +This integration currently uses [Rivet's Flue fork](https://github.com/rivet-dev/flue). +We're working to merge its generic target-authoring and runtime extension APIs +[upstream](https://github.com/withastro/flue/discussions/516), allowing Flue to +support actor-model runtimes without taking a Rivet dependency. + + +[View the complete example →](https://github.com/rivet-dev/agentos/tree/main/examples/flue) + +## Quickstart + + + + + +```sh +mkdir my-agent && cd my-agent +npm init -y +npm pkg set type=module +npm add "@flue/runtime@npm:@rivet-dev/labs-flue-runtime" +npm add --save-dev "@flue/cli@npm:@rivet-dev/labs-flue-cli" +npx flue init --target node +``` + + + + + +```sh +npm add @rivet-dev/flue @rivet-dev/agentos @rivet-dev/agentos-flue rivetkit +``` + +- `@rivet-dev/labs-flue-*`: Rivet-maintained preview builds of Flue's proposed extension APIs. +- `@rivet-dev/flue`: Runs Flue agents and workflows as Rivet Actors. +- `@rivet-dev/agentos`: Provides the isolated VM actor. +- `@rivet-dev/agentos-flue`: Connects Flue's sandbox API to agentOS. + + + + + +Create `actors.ts`: + +```ts title="actors.ts" +import { agentOS, setup } from "@rivet-dev/agentos"; + +const vm = agentOS({ + // Configure software, permissions, mounts, and resource limits here. +}); + +export const registry = setup({ + use: { vm }, +}); +``` + +Update `flue.config.ts`: + +```ts title="flue.config.ts" +import { defineConfig } from "@flue/cli/config"; +import { rivet } from "@rivet-dev/flue"; + +export default defineConfig({ + target: rivet({ actors: "./actors.ts" }), +}); +``` + +The generated Flue server adds its agent and workflow actors to this registry. +It keeps Flue's native router as the public HTTP service; the Rivet target only +selects and hosts the durable actors behind those routes. + + + + + +Create `agents/assistant.ts`: + +```ts title="assistant.ts" +import { type AgentRouteHandler, createAgent } from "@flue/runtime"; +import { agentOSSandbox } from "@rivet-dev/agentos-flue"; +import { registry } from "../actors.js"; + +export const route: AgentRouteHandler = async (_context, next) => next(); + +export default createAgent(() => ({ + model: "anthropic/claude-sonnet-5", + instructions: + "Help the user work in /workspace. Use filesystem and shell tools when asked.", + sandbox: agentOSSandbox({ actor: "vm", registry }), +})); +``` + +Set the provider key required by your model, such as `ANTHROPIC_API_KEY`, in `.env`. + + + + + +```sh +npx flue connect assistant local +``` + +Flue builds the Rivet target, starts the local Rivet engine, and connects to the `assistant/local` actor. + +Ask it to use both filesystem and shell operations: + +> Write `hello from Flue` to `/workspace/hello.txt`, run `wc -c +> /workspace/hello.txt`, then read the file back. + +Reconnect to `assistant/local` and ask it to read the file again. The same Flue +context reconnects to the same agentOS actor and persistent filesystem. + + + + + + + + + + + +## Runtime model + +Each Flue agent instance and workflow run has its own Rivet Actor and SQLite database. Direct prompts and `dispatch()` inputs use the same durable admission path, so accepted work can recover after interruption. + +Normal agent and workflow requests return their `202` receipt after persisting the admission, its canonical input event, and a recovery alarm. Rivet then owns the turn as background work through `c.keepAwake(...)`; the caller does not remain attached while the model or workflow runs. Workflow requests with `?wait=result` are the explicit exception and stay open until the result is available. + +The agentOS adapter derives a stable VM actor key from the Flue context ID. Reusing a context reconnects to the same durable `/workspace` filesystem. + + +agentOS does not support Cloudflare Workers yet. It works with Node.js, Bun, or +Deno on platforms such as Railway, Kubernetes, or Vercel. + + +## Configuration + +`rivet()` accepts an optional `actors` module path, defaulting to `./actors.ts`. That module must export a `registry` created with `setup()`; the target adds its generated actors to that registry. + +`agentOSSandbox()` accepts: + +| Option | Required | Description | +| --- | --- | --- | +| `actor` | Yes | agentOS actor name from the registry, such as `vm`. | +| `registry` | Yes | The same application registry exported from `actors.ts`. | +| `params` | No | Parameters passed when connecting to a new agentOS actor. | +| `cwd` | No | Sandbox working directory. Defaults to `/workspace`. | +| `client` | No | Existing client configured for the same registry. | + +[Read the agentOS + Flue documentation →](https://agentos-sdk.dev/docs/frameworks/flue) diff --git a/vendor/actors/docs/content/integrations/index.mdx b/vendor/actors/docs/content/integrations/index.mdx new file mode 100644 index 0000000..362e7ad --- /dev/null +++ b/vendor/actors/docs/content/integrations/index.mdx @@ -0,0 +1,9 @@ +--- +title: "Integrations" +description: "Frameworks and platforms that work with Rivet Actors." +--- + +import { IntegrationCards } from "@/components/docs/IntegrationCards"; + + + diff --git a/vendor/actors/docs/content/integrations/vercel-eve.mdx b/vendor/actors/docs/content/integrations/vercel-eve.mdx new file mode 100644 index 0000000..57d5af7 --- /dev/null +++ b/vendor/actors/docs/content/integrations/vercel-eve.mdx @@ -0,0 +1,180 @@ +--- +title: "Vercel Eve" +description: "Use Rivet as the durable World for Vercel Eve." +skill: false +--- + +import { Hosting } from "@/components/docs/Hosting"; + +{/* + Keep this guide synchronized with + rivet-dev/agentos/website/src/content/docs/docs/frameworks/vercel-eve.mdx. + This intentionally hardcodes the agentOS example instead of using CodeSnippet: + importing snippets across repositories would add an agentOS dependency to Rivet. + The default walkthrough demonstrates agentOS and Rivet World together. +*/} + + +This integration is in beta. APIs may change between releases. + + +Eve owns the agent runtime and session lifecycle, while agentOS maps every sandbox session to an isolated VM actor with a durable `/workspace` filesystem. + +[View the complete example →](https://github.com/rivet-dev/agentos/tree/main/examples/vercel-eve) + +## Quickstart + + + + + +```sh +npx eve@latest init my-agent +cd my-agent +``` + + + + + +```sh +npm add @rivet-dev/agentos @rivet-dev/agentos-eve @rivet-dev/vercel-world +``` + +- `@rivet-dev/agentos`: Provides the agentOS VM. +- `@rivet-dev/agentos-eve`: Connects Eve's sandbox API to agentOS. +- `@rivet-dev/vercel-world`: Runs Eve workflows on [Rivet World](https://workflow-sdk.dev/worlds). + + + + + +Update `agent/agent.ts`: + +```ts title="agent/agent.ts" +import { defineAgent } from "eve"; + +export default defineAgent({ + model: "anthropic/claude-sonnet-5", + build: { + externalDependencies: [ + "@rivet-dev/agentos", + "@rivet-dev/agentos-core", + "@rivet-dev/agentos-eve", + "@rivet-dev/agentos-runtime-core", + "@rivet-dev/agentos-sidecar", + "@rivet-dev/vercel-world", + "@rivetkit/engine-cli", + ], + }, + experimental: { + workflow: { world: "#world" }, + }, +}); +``` + + + + + +Rivet World lets you run Eve on top of Rivet. + +Add the World module import to `package.json`: + +```json title="package.json" +{ + "imports": { + "#world": "./world.ts" + } +} +``` + +Create `world.ts`: + +```ts title="world.ts" +import { createWorld as createRivetWorld } from "@rivet-dev/vercel-world"; +import { registry } from "./actors"; + +export const createWorld = () => createRivetWorld({ registry }); +``` + +The first World operation starts this registry and waits for the Rivet envoy to +be ready. + + + + + +Create `actors.ts`: + +```ts title="actors.ts" +import { agentOS, setup } from "@rivet-dev/agentos"; +import { vercelWorldActors } from "@rivet-dev/vercel-world/registry"; + +const vm = agentOS({ + // Configuration will go here. +}); + +export const registry = setup({ + use: { + ...vercelWorldActors, + vm, + }, +}); +``` + +Create `agent/sandbox.ts`: + +```ts title="agent/sandbox.ts" +import { agentOSBackend } from "@rivet-dev/agentos-eve"; +import { defineSandbox } from "eve/sandbox"; +import { registry } from "../actors"; + +export default defineSandbox({ + backend: agentOSBackend({ actor: "vm", registry }), +}); +``` + + + + + +Link Eve to Vercel once so it can call your configured model: + +```sh +npx eve link +``` + +Then run the agent: + +```sh +npx eve dev +``` + + + + + + + + + + + +## Using with other sandbox providers + +agentOS is a drop-in replacement for any Eve sandbox backend. + +[Read the Eve sandbox documentation →](https://eve.dev/docs/sandbox) + +## Configuring agentOS with Eve + +`agentOS()` configures the VM's filesystems, software, extensions, and limits. + +[Read the agentOS + Vercel Eve documentation →](https://agentos-sdk.dev/docs/frameworks/vercel-eve) + +## Configuring Rivet World with Eve + +Rivet World stores Eve's runs in Rivet Actors so agents resume instead of restarting. + +[Read the Rivet World + Vercel Eve documentation →](/integrations/vercel-workflows) diff --git a/vendor/actors/docs/content/integrations/vercel-workflows.mdx b/vendor/actors/docs/content/integrations/vercel-workflows.mdx new file mode 100644 index 0000000..69c8a05 --- /dev/null +++ b/vendor/actors/docs/content/integrations/vercel-workflows.mdx @@ -0,0 +1,170 @@ +--- +title: "Vercel Workflows (Beta)" +description: "Vercel Workflows backed by Rivet Actors." +skill: false +--- + + +This integration is in beta. APIs may change between releases. + + +`@rivet-dev/vercel-world` implements the Vercel [World](https://workflow-sdk.dev/worlds) API with native Rivet Actors. + +[View the complete example →](https://github.com/rivet-dev/rivet/tree/main/examples/vercel-workflow) + +## Quickstart + + + + + +Set up a Vercel Workflows project for your framework on Node.js 22 or newer. The +[Workflows getting-started guides](https://workflow-sdk.dev/docs/getting-started) +cover Next.js, Astro, Express, Fastify, Hono, Nitro, Nuxt, SvelteKit, TanStack +Start, and Vite. + + + + + +```sh +npm install workflow @rivet-dev/vercel-world +``` + + + + + +Create `workflows/order.ts`: + + + + + + + +The World starts Rivet lazily in the Workflows process. You do not need a +second server or a framework instrumentation hook. + + + + + +Create `app/api/orders/[id]/route.ts`: + +```ts app/api/orders/[id]/route.ts +import { start } from "workflow/api"; +import { processOrder } from "@/workflows/order"; + +export async function POST( + _request: Request, + { params }: { params: Promise<{ id: string }> }, +) { + const { id } = await params; + const run = await start(processOrder, [id]); + return Response.json({ runId: run.runId }); +} +``` + + + + + +Hono has no build system of its own, so use Nitro to compile the workflow and +serve its handler in the same process. + +Create `nitro.config.ts`: + + + +Create `src/server.ts`: + + + + + + + +Create `.env` with the variables under [Configuration](#configuration), then +build and run: + +```sh +npm run build +npm run dev +``` + + + + + +Start a run: + +```sh +curl -X POST http://localhost:3000/orders/42 +``` + +Use Vercel's Workflow Vitest harness. The first World operation starts the +native Rivet registry and Engine in the test process: + +```sh +npm test +``` + + + + + +## Configuration + +Select the World and configure its Rivet connection in the application process: + + + +| Variable | Required | Purpose | +| --- | --- | --- | +| `WORKFLOW_TARGET_WORLD` | Yes | Loads `@rivet-dev/vercel-world` through Vercel Workflows | +| `WORKFLOW_RUNTIME_URL` | Yes | Externally reachable base URL of the Workflows HTTP server | +| `WORKFLOW_QUEUE_NAMESPACE` | No | Shared queue namespace used by Workflows and crash-safe initial dispatch | +| `RIVET_ENDPOINT` | Remote only | Rivet control plane endpoint | +| `RIVET_NAMESPACE` | Remote only | Namespace containing the World actors | +| `RIVET_POOL` | Remote only | Pool that hosts the native World registry | +| `RIVET_TOKEN` | Cloud only | Token sent to Rivet | +| `RIVET_WORKFLOW_SECRET` | Recommended when public | Shared bearer secret for World-to-runtime delivery | + +Local development starts the native Rivet control plane automatically. For production, +configure the remote Rivet connection. Run the combined server at +`WORKFLOW_RUNTIME_URL`; its World client and native registry use the same Rivet +endpoint, namespace, and pool. + +## HTTP routes + +Your framework integration serves the combined workflow handler at +`.well-known/workflow/v1/flow`. `WORKFLOW_RUNTIME_URL` must resolve to the +service hosting that route. Do not point it at the Rivet control plane. If +`RIVET_WORKFLOW_SECRET` is set, delivery carries that value as a bearer token and +rejects requests without it. + +## Durability + +The World stores runs, event logs, queues, streams, hook tokens, and recovery +alarms in Rivet Actors. + +Recovery is local to each run; startup does not scan all actors. Queue an initial +workflow with the default namespace or `WORKFLOW_QUEUE_NAMESPACE`. The current +Vercel Workflows does not include a per-call `start({ namespace })` value in the +`run_created` event, so that per-call override cannot be reconstructed after a +crash and is not supported by this World. + +Application code continues to use `"use workflow"`, `"use step"`, +`workflow/api`, hooks, sleeps, and streams exactly as documented by Vercel. + +## Testing + + + + + +`waitForSleep` observes the durable sleep, `wakeUp` resumes that exact +correlation, and the assertions wait for the final persisted result. + +See the [Vercel Workflows documentation](https://useworkflow.dev) for the SDK itself. diff --git a/vendor/actors/docs/content/learn/a-radically-simpler-architecture.mdx b/vendor/actors/docs/content/learn/a-radically-simpler-architecture.mdx new file mode 100644 index 0000000..6648a02 --- /dev/null +++ b/vendor/actors/docs/content/learn/a-radically-simpler-architecture.mdx @@ -0,0 +1,136 @@ +--- +title: "A Radically Simpler Architecture" +description: "Why actors eliminate complexity instead of managing it, and how merging state and compute removes the biggest source of latency in modern applications." +--- + +The typical backend architecture follows a familiar pattern. A web server connects to a database. Traffic grows, so you add Redis for caching. You need async processing, so you add Kafka. You need coordination, so you add distributed locks. + +## Every Solution Creates A Problem + +As you add components to your architecture to solve problems as you scale, in turn you create new problems for yourself: + +- **Caching**: brings cache invalidation bugs, stale data, and thundering herd problems. +- **Message queues**: bring message ordering issues, exactly-once delivery problems, and dead letter queue monitoring. +- **Pub/sub systems**: bring subscription management complexity, message replay challenges, and coordination overhead. +- **Distributed locks**: bring deadlocks, lock timeouts, and split-brain scenarios. +- **Multiple services**: bring distributed transactions, eventual consistency, and network partition handling. + +Worse, these are bugs you can't unit test for. They're emergent behaviors that only appear under load when it matters most. + +Yet it's accepted as _the way things must be_. Nobody got fired for adding Kafka, Redis, and RabbitMQ to the stack. The fact that each one brings its own failure modes is assumed to be the growing pains of any successful business. + +Traditional backend architecture with separate layers for web server, cache, database, and message queue + +--- + +## How We Got Here + +Looking back at the very first thing you did when starting your application: setting up a web server and a database. The way you've designed your app is through an **age-old practice of "separating state and compute."** + +We've been doing it this way since the 1980s, when client-server architecture put databases on their own machines. + +This came from the fact that computers were slow and had limited resources. Running application code and database operations on the same machine meant they'd fight over CPU and memory, making both perform poorly. Separating them protected databases from compute overhead. + +This tradeoff made sense when CPU and memory were severely limited, but the pattern outlived its purpose. As traffic grew, we added caching layers, message queues, and distributed locks — each solving a problem from the last without questioning the original assumption of how we got here. + +## 40 Years Later + +Those constraints from forty years ago no longer apply to today's servers. Modern CPUs are orders of magnitude faster, and memory is abundant and cheap. **Application bottlenecks have shifted from local compute to network latency and locks.** + +This is best demonstrated with a simple comparison between a real-world Postgres query over the network versus a SQLite query on the same machine: A Postgres query over the network takes 1-10ms over LAN. The same query on a local SQLite database running in the same process as your application takes 0.01-0.1ms, **roughly 100x faster**. (These benchmarks are heavily dependent on the workload, this is a conservative performance number for SQLite.) + +That 100x difference is not about switching to a marginally different database, it's about rethinking your architecture for modern computers by eliminating the centralized database completely in favor of databases colocated with your compute. **Combining compute and state removes the biggest sources of latency in modern applications.** + +--- + +## The Actor Model: Combining Compute and State + +Actors take the completely opposite approach to "separating compute and state:" they **merge state and compute together**. + +Each actor's **state is isolated to itself** and cannot be read by any other actors. Instead, you communicate with actors over the network via actions. + +They're like mini-servers: they can accept and respond to network requests and even send network requests themselves. They remain running as a long-lived process with in-memory state until they decide to go to sleep. + +In addition to performance and complexity benefits, this architecture **eliminates entire categories of bugs by design.** No network to the database means no network partitions. No shared state means no race conditions. No locks means no deadlocks. + +Actor architecture showing compute and state combined in isolated actors + +## The 4 Properties That Eliminate Complexity + +By combining compute and state, actors present a few key properties that eliminate entire categories of problems. These properties are the core of the design patterns that we'll discuss in further articles. + +### Isolated State + +Each actor **manages its own private state**. No other process can access an actor's state. + +This eliminates race conditions (can't happen when only one process touches the data), deadlocks (no locks means no deadlocks), cache invalidation (no shared cache to invalidate), and read-after-write inconsistencies (your writes are immediately visible to you). + +Debugging becomes straightforward: the actor's state is the single source of truth. There's no need to reconstruct state from multiple systems or reason about eventual consistency across caches, databases, and message queues. + +As your app grows, new features affect a limited number of actors which have a limited scope. Changes don't ripple through shared state across services or risk breaking unrelated parts of your system. + +Diagram showing actors with isolated state that cannot be accessed by other processes + +### Message-Based Communication + +Actors **talk through actions and events**, not direct state access. This makes it easier to scale actors since they can scale horizontally across multiple machines and still communicate efficiently. + +Messages sent to actors are **automatically queued and processed sequentially**. This almost always eliminates the need for external message queues since backpressure, ordering, and delivery are handled by the actor runtime itself. + +Crucially, **actors frequently talk to each other** to build larger systems that scale well. We'll be talking a lot about patterns like this in this course. + +Diagram showing actors communicating through messages and events + +### Location Transparency + +Actors can run on any machine in a cluster and still **send messages between actors regardless of the host machine**. Rivet automatically handles intelligent load balancing of actors and routing between actors. + +The same code will run whether you have 1 or 1,000 machines without complex network configuration, DNS, or pub/sub systems. + +Diagram showing actors communicating across different machines in a cluster + +### Horizontal Scaling + +Actors are designed to transparently interact with other actors regardless of what machine they run on. This makes actors easy to scale by **just adding more machines** for actors to run on when you need it. + +Load spreads naturally since actors are small, lightweight units. No complex sharding logic or coordination needed. + +Diagram showing actors distributed across multiple machines for horizontal scaling + +--- + +## Putting It All Together: A Radically Simpler Architecture + +When you build your backend with actors, the four properties listed remove the need for: + +- **Redis/Memcached**: Caching is built-in (state already lives in-memory with compute). +- **Kafka/RabbitMQ/SQS**: Message queueing, events, and async messaging are built-in to the actor runtime. +- **NATS/Redis Streams**: Pub/sub is built-in to actors through message passing and events. +- **Consul/etcd/ZooKeeper**: No distributed coordination needed, actors encapsulate their own state and the runtime handles discovery and routing automatically. +- **Istio/Linkerd**: Actors handle routing and discovery automatically. +- **Database sharding**: Actors distribute themselves automatically. No shard keys, no rebalancing logic, no cross-shard queries. + +## If Actors Are So Great, Why Aren't They Everywhere? + +If you've reached this point and are unfamiliar with the actor model, you're probably asking this exact question. It all sounds a little _too_ rosy. + +The truth is that actors _are_ used widely — just not visibly. Large enterprises with engineers who've spent years wrestling with traditional architectures have long since adopted them. The pattern has proven itself at massive scale: + +- WhatsApp (notoriously acquired for $19B running Erlang/OTP with only 35 engineers) +- Discord +- LinkedIn +- X +- Pinterest +- PayPal +- FoundationDB (powering Apple, Snowflake, DataDog) + +So why hasn't the actor model spread to smaller teams and mainstream development? + +This mirrors TypeScript's trajectory. It started as a niche tool for large codebases — most developers dismissed it as unnecessary overhead with poor tooling. But as more developers felt the pain of loose typing at scale, adoption grew. Today, TypeScript is a non-negotiable for many teams because of that collective suffering. + +Actors are on the same trajectory. The pain of distributed systems complexity is becoming impossible to ignore. + +Other ecosystems have had mature actor frameworks for years — Erlang has OTP, Java has Akka, C# has Microsoft Orleans. But TypeScript has been the missing piece until recently with: + +- **Rivet Actors**: Open-source actor infrastructure for TypeScript +- **Cloudflare Durable Objects**: Leverages Cloudflare's existing network & JavaScript runtime diff --git a/vendor/actors/docs/content/learn/ai-agent.mdx b/vendor/actors/docs/content/learn/ai-agent.mdx new file mode 100644 index 0000000..8049ffe --- /dev/null +++ b/vendor/actors/docs/content/learn/ai-agent.mdx @@ -0,0 +1,133 @@ +--- +title: "AI Agent" +description: "Build an AI agent backend with persistent memory: one Rivet Actor per conversation, queued message handling, and streaming LLM responses as realtime events." +templates: ["ai-agent"] +--- + +Patterns for building AI agent backends with RivetKit, where each conversation is one Rivet Actor that owns its memory, its message queue, and its streaming output. + +## Starter Code + +Start with one of the working examples on [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/ai-agent) and adapt it. The sections below describe the flagship `ai-agent` example unless a variant is called out explicitly. + +| Variant | Starter Code | Use When | +| --- | --- | --- | +| Queue-driven AI SDK agent | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/ai-agent) | You want a streaming chat agent where each conversation keeps its own persistent memory and processes one message at a time. | +| Sandbox coding agent | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/sandbox-coding-agent) | The agent should run a coding agent (Codex by default) inside an isolated [sandbox](/agentos/docs) via Docker, Daytona, or E2B. | +| Durable streams agent (experimental) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/experimental-durable-streams-ai-agent) | You want replayable, restart-safe prompt and response delivery through durable streams instead of actor state and events. | + +## Conversation Memory + +Use one actor per conversation, keyed by a conversation or agent id (see [Actor Keys](/actors/docs/keys)). The agent actor's persistent [state](/actors/docs/state) is the conversation memory: in the `ai-agent` example, `messages` and `status` live in JSON actor state and survive sleep and restarts with no external database. Every model call rebuilds the prompt from `c.state.messages` plus a system prompt, so memory and inference input are the same data. + +| Variant | Where Memory Lives | Persisted State Fields | +| --- | --- | --- | +| `ai-agent` | JSON actor state | `messages`, `status` | +| `sandbox-coding-agent` | JSON actor state plus the sandbox ACP session | `messages`, `status`, `sessionId` | +| `experimental-durable-streams-ai-agent` | Durable streams; the actor stores only its conversation id and a read cursor | `conversationId`, `promptStreamOffset` | + +## Message Handling + +In the `ai-agent` example, the client pushes user input onto the agent's `message` [queue](/actors/docs/queues) with `agent.connection.send("message", { text, sender })`. This is a queue push, not an action call. The actor's `run` hook (see [Lifecycle](/actors/docs/lifecycle)) consumes the queue serially with `for await (const queued of c.queue.iter())`. + +Serial queue consumption is the per-conversation concurrency guarantee: at most one in-flight model call per actor, with no extra locking. The `status` field (`thinking` while a model call is in flight) is UI signal only; the run loop is the actual lock. The loop also checks `c.aborted` inside the token stream so shutdown exits gracefully. + +| Variant | Message Ingress | Serialization Guarantee | +| --- | --- | --- | +| `ai-agent` | `message` queue pushed via `connection.send` | `run` hook pops one queued message at a time with `c.queue.iter()`. | +| `sandbox-coding-agent` | `sendMessage` [action](/actors/docs/actions), no queue | Each call awaits the sandbox round trip before broadcasting the result. | +| `experimental-durable-streams-ai-agent` | Durable prompt stream long-polled from `onWake` | `promptStreamOffset` is persisted per chunk, so restarts resume without reprocessing prompts. | + +## Streaming Responses + +The `ai-agent` actor broadcasts a `response` [event](/actors/docs/events) for every model text delta. The payload carries `messageId`, the per-token `delta`, the cumulative `content`, and a `done` flag (plus `error` on failure), so clients can either append deltas or idempotently replace the message by `messageId` using `content`. The example frontend replaces by `messageId`, which tolerates dropped events. The terminal broadcast has an empty `delta`, the full `content`, and `done: true`. + +Because the assistant message object lives in `c.state.messages` and is mutated in place during streaming, partial content persists if the actor restarts mid-stream. The example broadcasts once per AI SDK delta with no throttling; batching or throttling deltas is a recommended extension for high-traffic deployments, not something the example implements. + +Variant differences: `sandbox-coding-agent` sends a single `response` broadcast with `done: true` after the sandbox finishes (no incremental streaming), and `experimental-durable-streams-ai-agent` appends per-token chunks to a durable response stream, then broadcasts `responseComplete` or `responseError`. + +## Architecture + +| Topic | Summary | +| --- | --- | +| Topology | `agentManager["primary"]` singleton directory plus one `agent[agentId]` actor per conversation. | +| Ingress | Client pushes `AgentQueueMessage` payloads onto the agent's `message` queue with `connection.send`. | +| Streaming | One `response` broadcast per model delta, terminal broadcast with `done: true`. | +| Memory | Full transcript and status in JSON actor state; no external database. | + +The manager creates `AgentInfo` records and warms each agent through [actor-to-actor communication](/actors/docs/communicating-between-actors): `createAgent` calls `c.client()`, then `client.agent.getOrCreate([info.id])` and awaits `getStatus()` so the conversation actor exists before the client connects. The sandbox variant extends this topology with a `codingSandbox` actor that shares the agent's key (`codingSandbox.getOrCreate([c.key[0]])`), so the agent-to-sandbox mapping is implicit in the key space. + +**Actors** + + + + +- **Key**: `agentManager["primary"]` +- **Responsibility**: Directory actor. Creates `AgentInfo` records, lists agents, and warms each agent actor via `c.client()`. +- **Actions** + - `createAgent` + - `listAgents` +- **Queues** + - None +- **State** + - JSON + - `agents` + + + + +- **Key**: `agent[agentId]` +- **Responsibility**: One actor per conversation. Holds the full message history and status, consumes queued user messages in its `run` loop, calls the model via the AI SDK, and broadcasts streaming deltas. +- **Actions** + - `getHistory` + - `getStatus` +- **Queues** + - `message` +- **Events** + - `messageAdded` + - `status` + - `response` +- **State** + - JSON + - `messages` + - `status` + + + + +**Lifecycle** + +```mermaid +sequenceDiagram + participant C as Client + participant AM as agentManager + participant A as agent + participant LLM as Model API + + C->>AM: createAgent(name) + AM->>A: getOrCreate([info.id]) + getStatus() + AM-->>C: AgentInfo + C->>A: connection.send("message", {text, sender}) + Note over A: run loop pops queue via c.queue.iter() + A-->>C: messageAdded (user message) + A-->>C: messageAdded (assistant placeholder) + A-->>C: status (thinking) + A->>LLM: streamText(system prompt + history) + loop each text delta + LLM-->>A: delta + A-->>C: response {messageId, delta, content, done: false} + end + A-->>C: response {delta: "", content, done: true} + A-->>C: status (idle) +``` + +## Security Checklist + +The examples ship without auth so they stay minimal. Apply this baseline before exposing an agent backend. + +- **API keys stay server-side**: `OPENAI_API_KEY` (or `ANTHROPIC_API_KEY`) is read by the AI SDK inside the actor process. The key never reaches the browser; clients only talk to the actor over RivetKit. The sandbox variant forwards keys into the sandbox env, never to the client. +- **Add authentication**: The examples have no auth, so anyone who reaches the server can create agents, list them, and message any agent whose key they can guess. Add `onBeforeConnect` or `createConnState` checks with scoped tokens as a recommended extension. See [Authentication](/actors/docs/authentication). +- **Validate and rate-limit queue payloads**: The example only skips bodies without a string `text`. Enforce payload size limits, schema validation, and per-connection rate limits as a recommended extension. +- **Derive sender identity server-side**: The example trusts the client-supplied `sender` field verbatim. Bind sender identity to the authenticated connection instead. +- **Cap or trim message history**: The example sends the full transcript on every model call with no cap. Trim or summarize old messages as a recommended extension so prompts and state stay bounded. +- **Set cost ceilings per conversation**: Add per-agent token budgets and quotas as a recommended extension. The sandbox variant runs real compute, so also enforce per-user sandbox quotas and restrict sandbox network egress. diff --git a/vendor/actors/docs/content/learn/chat-room.mdx b/vendor/actors/docs/content/learn/chat-room.mdx new file mode 100644 index 0000000..8d47f38 --- /dev/null +++ b/vendor/actors/docs/content/learn/chat-room.mdx @@ -0,0 +1,117 @@ +--- +title: "Chat Room" +description: "Build a realtime chat room backend with Rivet Actors: one actor per room, SQLite-backed message history, and WebSocket broadcast to every connected client." +templates: ["chat-room"] +--- + +Patterns for building a chat room backend with RivetKit: room-scoped actors, persistent message history, and realtime delivery over WebSocket connections. + +## Starter Code + +Start with the working example on [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/chat-room) and adapt it. The backend is a single `chatRoom` actor; the frontend is a React app using `@rivetkit/react` (see the [React quickstart](/actors/docs/quickstart/react)). + +| Topic | Summary | +| --- | --- | +| Room model | One `chatRoom` actor per room key. The frontend defaults the key to `general`; typing a different room name connects to a different actor. | +| History | SQLite `messages` table created in `db({ onMigrate })`, read back with `ORDER BY id ASC`. | +| Delivery | `sendMessage` inserts the row, then broadcasts a typed `newMessage` event to every connected client. | +| Identity | None in the example. `sender` is a plain action argument; production should bind identity to the connection. | + +## Room-Per-Actor Model + +Each room is one Rivet Actor instance, addressed by [key](/actors/docs/keys). The client calls `useActor({ name: "chatRoom", key: [roomId] })`, which gets-or-creates the actor for that room. This gives you: + +- **Isolation**: each room's history and connections are fully scoped to its key. Switching the room input re-keys the hook and connects to a different actor with separate history. +- **A single serialized writer**: all `sendMessage` calls for one room run through one actor, so message ordering is consistent without locks. The SQLite `AUTOINCREMENT` id is the canonical order, which is why `getHistory` sorts by `id` rather than by timestamp. +- **Natural scaling**: rooms spread across the cluster independently. A hot room does not slow down other rooms. + +## Message History Storage + +This example stores history in the actor's SQLite database, not in JSON state. Pick based on history size and query needs: + +| Approach | Use When | Implementation Guidance | +| --- | --- | --- | +| [SQLite](/actors/docs/sqlite) (what this example uses) | Large or long-lived history that needs ordering, caps, pagination, or search | Create the `messages` table in `db({ onMigrate })`, insert with parameterized queries (`c.db.execute("INSERT ... VALUES (?, ?, ?)", ...)`), and read with `ORDER BY id ASC`. History survives actor sleep and scales past what you want in memory. | +| [JSON state](/actors/docs/state) | Small recent history, for example the last 50 to 100 messages | Push onto a `messages` array in actor state and trim to a cap on every send. Simplest option, but the whole history lives in memory and there is no query layer, so it only fits bounded recent-history use cases. | + +## Broadcast Delivery + +New messages reach connected clients through a typed [event](/actors/docs/events): + +- The actor declares `events: { newMessage: event() }`, where `Message` is `{ sender, text, timestamp }`. +- The `sendMessage` [action](/actors/docs/actions) builds the message with a server-side `Date.now()` timestamp, inserts it into the `messages` table, then calls `c.broadcast("newMessage", message)` and returns the message to the caller. +- Each client subscribes with `useEvent("newMessage", ...)` and appends to its local list. The sender renders its own message through the same broadcast path as everyone else, so all clients stay on one code path. +- History load is connection-gated: once the connection is ready, the client calls `getHistory()` once to render the backlog, then relies on events for everything after. + +Use `c.broadcast(...)` for room-wide messages. For private or per-recipient payloads (such as DMs inside a room), send on the individual connection instead, which is a recommended extension beyond this example. + +## Typing Indicators And Presence (Extension) + +The example does not implement typing indicators, presence, or join/leave handling of any kind. There is no `createConnState`, `onConnect`, or `onDisconnect` in the code. If you need them, add them as ephemeral [connection](/actors/docs/connections) behavior: + +- **Keep it ephemeral**: store the username and typing flag in per-connection state, never in SQLite or persisted actor state. Presence is derived from live connections and should disappear with them. +- **Broadcast on change only**: emit a typing event when a user starts or stops typing, and a presence event from `onConnect` / `onDisconnect`, rather than polling or ticking. +- **Expire on the client**: clear a typing indicator after a short client-side timeout so a dropped connection never leaves a stuck "is typing" row. + +## Per-User Inbox (Extension) + +For offline delivery, DMs, unread counts, or notification fanout, add a `userInbox[userId]` actor per user. This is an extension beyond the example: + +- The room actor forwards each message to the inbox actor of every member via [actor-to-actor calls](/actors/docs/communicating-between-actors), so users who are not connected to the room still accumulate messages. +- The inbox actor owns per-user unread state and serves it when the user comes online, independent of which rooms they are in. +- DMs become a degenerate room: either a `chatRoom` keyed by the sorted pair of user ids, or direct inbox-to-inbox delivery if you do not need shared history semantics. + +## Actors + + + + +- **Key**: `chatRoom[roomId]` +- **Responsibility**: Owns one chat room. Persists the room's message history in its SQLite database and broadcasts each new message to every connected client. +- **Actions** + - `sendMessage` + - `getHistory` +- **Queues** + - None +- **Events** + - `newMessage` +- **State** + - SQLite + - `messages` table: `id` (autoincrement primary key), `sender`, `text`, `timestamp` + + + + +## Lifecycle + +```mermaid +sequenceDiagram + participant A as Client A + participant B as Client B + participant R as chatRoom + + A->>R: connect with key [roomId] + Note over R: every start runs onMigrate (CREATE TABLE IF NOT EXISTS messages) + A->>R: getHistory() + R-->>A: Message[] ordered by id + B->>R: connect with key [roomId] + B->>R: getHistory() + R-->>B: Message[] ordered by id + A->>R: sendMessage(sender, text) + Note over R: INSERT row with server timestamp + R-->>A: newMessage (broadcast) + R-->>B: newMessage (broadcast) + A->>R: disconnect + Note over R: history stays in SQLite for the next connection +``` + +## Security Checklist + +The example is intentionally minimal and skips all of the following. Add them before production: + +- **Auth before join**: any client can join any room by knowing its name, and `sender` is arbitrary client input on every call. Validate a token during [connection auth](/actors/docs/authentication), bind identity to [connection state](/actors/docs/connections), and check room membership before serving history. Never trust a sender name passed as an action argument. +- **Message length clamps**: the example accepts empty messages and has no length limit. Trim server-side, reject empty text, and clamp to a maximum length. +- **Per-connection rate limiting**: rate limit `sendMessage` per connection to stop spam and broadcast amplification. +- **Server-side timestamps and ids**: the example already does this correctly. `timestamp` comes from `Date.now()` inside the action and `id` from SQLite `AUTOINCREMENT`. Keep it that way; never accept client-supplied timestamps or ids. +- **History caps**: `getHistory` returns every row with no limit. Add a `LIMIT` plus pagination, and prune or archive old rows so a long-lived room cannot grow unbounded. +- **Parameterized queries**: the example already inserts with `?` placeholders. Keep all user-supplied text out of SQL string interpolation. diff --git a/vendor/actors/docs/content/learn/collaborative-text-editor.mdx b/vendor/actors/docs/content/learn/collaborative-text-editor.mdx new file mode 100644 index 0000000..1504e1b --- /dev/null +++ b/vendor/actors/docs/content/learn/collaborative-text-editor.mdx @@ -0,0 +1,163 @@ +--- +title: "Collaborative Text Editor" +description: "Build a collaborative text editor backend with Yjs CRDTs and Rivet Actors: per-document actors relay sync and awareness updates and persist snapshots." +templates: ["collaborative-document"] +--- + +Patterns for building a Yjs server on RivetKit: CRDT document sync, presence and cursors, and snapshot persistence, with one Rivet Actor per document acting as a relay. + +## Starter Code + +Start with the working example on [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/collaborative-document) and adapt it to your editor. It ships a React frontend with a plain textarea, remote cursor overlays, and a workspace document index. + +| Use Case | Starter Code | Common Examples | +| --- | --- | --- | +| Shared document editing | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/collaborative-document) | Notion-style docs, shared notes, pair-writing tools, form co-editing | + +## CRDT vs OT + +Two families of algorithms solve concurrent text editing. The choice decides what your server has to do. + +| Dimension | CRDT (Yjs) | Operational Transformation | +| --- | --- | --- | +| Conflict resolution model | Commutative merges. Updates apply in any order on any peer and converge to the same result. | Server transforms each operation against every concurrent operation. Correctness depends on a central sequencer. | +| Offline support | Strong. Clients keep editing locally and merge buffered updates on reconnect. | Weak. Long-lived divergence makes transformation chains complex and fragile. | +| Server role | Relay plus persistence. The server applies opaque updates and rebroadcasts them. It never needs to understand document semantics. | Authoritative transformer. The server must implement transformation logic for every operation type. | +| Library maturity | Yjs is mature and widely deployed, with bindings for ProseMirror, CodeMirror, Monaco, and others. | Production-grade implementations are mostly proprietary (Google Docs) or aging (ShareDB). | + +The example uses Yjs because CRDTs let the server stay a relay-style Rivet Actor. The actor applies each incoming update to a server-side `Y.Doc` so it can persist the merged state and serve late joiners, but it never transforms operations or arbitrates conflicts. Ordering does not matter because Yjs merges are commutative. + +## Document Actor Model + +| Topic | Summary | +| --- | --- | +| Topology | One `document[workspaceId, documentId]` actor per document plus one `documentList[workspaceId]` coordinator per workspace. | +| Sync model | Each client holds a local `Y.Doc`. The document actor relays incremental Yjs updates as broadcast [events](/actors/docs/events) and keeps a server-side merged copy in vars. | +| Persistence | Full merged Yjs snapshot overwritten in one binary [actor KV](/actors/docs/kv) key (`yjs:doc`) on every sync update. Document metadata lives in JSON [state](/actors/docs/state). | +| Queues | None. The example is purely [actions](/actors/docs/actions) plus broadcast events. | +| Presence | Yjs Awareness relayed through the same `applyUpdate` action. Per-connection `connState` tracks asserted awareness clientIds for disconnect cleanup. | + +The two-actor split follows the coordinator pattern from [Design Patterns](/actors/docs/design-patterns): the coordinator owns discovery and creation, and each document actor owns one document's realtime state. Multi-part [keys](/actors/docs/keys) scope both actors to a workspace. + +**Actors** + + + + +- **Key**: `document[workspaceId, documentId]` +- **Responsibility**: Applies incoming sync and awareness updates to a server-side `Y.Doc` and `Awareness`, persists the merged Yjs snapshot to actor KV, and broadcasts updates to all connected collaborators. +- **Actions** + - `getContent` + - `applyUpdate` + - `getAwareness` +- **Queues** + - None +- **State** + - JSON metadata only: `title`, `createdAt`, `updatedAt` + - Binary KV key `yjs:doc` holding the full merged Yjs snapshot + - Ephemeral vars: the live `Y.Doc` and `Awareness`, created in `createVars` and rehydrated from KV on actor start + - Per-connection `connState`: `clientIds` of awareness clients asserted by that connection + + + + +- **Key**: `documentList[workspaceId]` +- **Responsibility**: Coordinator for one workspace. Creates document actors through the actor-to-actor client and maintains the index of document summaries. +- **Actions** + - `createDocument` + - `listDocuments` + - `deleteDocument` +- **Queues** + - None +- **State** + - JSON + - `documents` array of `DocumentSummary` entries (`id`, `title`, `createdAt`, `updatedAt`) + + + + +The coordinator's `createDocument` generates a UUID, then explicitly creates the document actor with `c.client()` and passes `{ title, createdAt }` as creation [input](/actors/docs/input), which the document actor's `createState` consumes. See [Communicating Between Actors](/actors/docs/communicating-between-actors) for the actor-to-actor client. + +## Update Relay + +A single `applyUpdate(update, kind, clientId?)` action handles both update kinds. Updates cross the action boundary as `number[]` byte arrays and are converted back to `Uint8Array` on each side. + +| Kind | Server Applies To | Persists | Broadcasts | +| --- | --- | --- | --- | +| `"sync"` | `c.vars.doc` via `Y.applyUpdate` with origin `"client"` | Full merged snapshot to KV key `yjs:doc`, then bumps `updatedAt` | `sync` event carrying the incremental update | +| `"awareness"` | `c.vars.awareness` via `applyAwarenessUpdate` with origin `"client"` | Nothing. Presence is ephemeral. | `awareness` event carrying the update | + +Note the asymmetry on the sync branch: the broadcast carries only the small incremental update, while the KV write stores the full merged document re-encoded with `Y.encodeStateAsUpdate`. + +Yjs origin tags are the echo guards that keep the relay loop-free: + +| Origin Tag | Set Where | Effect | +| --- | --- | --- | +| `"local"` | Client edits inside `doc.transact(..., "local")` | The client's update listener fires and sends `applyUpdate` to the actor. | +| `"client"` | Server applying an incoming update to its `Y.Doc` or `Awareness` | Marks the change as client-originated on the server copy. | +| `"remote"` | Client applying broadcast events or initial sync data | Update listeners early-return on `"remote"`, so a client never re-sends its own echo. | + +On connect or reconnect, the client calls `getContent` and `getAwareness`, then applies both results to its local `Y.Doc` and `Awareness` with origin `"remote"`. After that, every change flows through `applyUpdate` and the broadcast events. + +## Awareness And Presence + +Presence (user names, colors, cursor positions) rides on the Yjs Awareness protocol instead of actor state: + +- Clients set presence with `awareness.setLocalStateField` for the `user` and `cursor` fields. The awareness update listener encodes the change and sends `applyUpdate(update, "awareness", awareness.clientID)`. +- The actor records each asserted `clientId` in that connection's `connState.clientIds`, applies the update to the server-side `Awareness`, and broadcasts the `awareness` event to all peers. See [Connections](/actors/docs/connections) for per-connection state. +- `onDisconnect` reads the connection's `clientIds`, calls `removeAwarenessStates` on the server-side `Awareness`, and broadcasts the encoded removal so every remaining client drops the departed user's cursor. See [Lifecycle](/actors/docs/lifecycle) for the hook. + +Because the actor tracks which awareness clientIds belong to which connection, presence cleanup is automatic on disconnect with no client cooperation required. + +## Persistence And Compaction + +The example persists with a full-snapshot overwrite: on every `"sync"` update, the actor re-encodes the entire merged document with `Y.encodeStateAsUpdate` and overwrites the single binary KV key `yjs:doc`. There is no append-only update log and no separate compaction job. Compaction is implicit because `Y.encodeStateAsUpdate` emits one compact merged representation of the document, so Yjs merge semantics keep the stored blob compact on their own. + +| Property | Full-Snapshot Overwrite (the example) | +| --- | --- | +| Write cost | One full-document KV write per sync update, so every keystroke rewrites the whole blob. | +| Read cost | One binary KV read in `createVars` rehydrates the document on actor start. | +| Crash safety | The last completed `applyUpdate` is durable. No log replay needed. | +| Sweet spot | Small to medium documents where simplicity beats write amplification. | + +**Recommended extension (not in the example)**: for large documents or very high edit rates, switch to appending incremental updates to a KV update log and writing a merged snapshot only periodically (for example every N updates). Boot becomes snapshot plus log replay, and the snapshot write becomes the explicit compaction step that truncates the log. Adopt this only when full-snapshot writes become the measured bottleneck or the blob approaches KV value size limits. + +## Lifecycle + +```mermaid +sequenceDiagram + participant A as Client A + participant B as Client B + participant DL as documentList + participant D as document + + A->>DL: listDocuments() + A->>DL: createDocument(title) + DL->>D: create([workspaceId, documentId], input) + DL-->>A: DocumentSummary + A->>D: connect + B->>D: connect + Note over D: createVars rehydrates Y.Doc from KV "yjs:doc" + A->>D: getContent() + getAwareness() + D-->>A: encoded doc + awareness state + Note over A: local edit with origin "local" + A->>D: applyUpdate(update, "sync") + Note over D: apply with origin "client", overwrite KV snapshot + D-->>B: sync event (incremental update) + Note over B: apply with origin "remote", no echo + A->>D: applyUpdate(update, "awareness", clientId) + D-->>B: awareness event + B-->>D: disconnect + Note over D: onDisconnect removes B's awareness clientIds + D-->>A: awareness event (removal) +``` + +## Security Checklist + +The example ships with no authentication or authorization. Harden it with this baseline before production. None of these are implemented in the example. + +- **Authenticate before connect**: Anyone who knows or guesses a workspace ID can connect, and because `useActor` implicitly getOrCreates, connecting with a nonexistent workspace ID silently creates a blank `documentList` coordinator. Add connection auth so unauthenticated clients never reach an actor. See [Authentication](/actors/docs/authentication). +- **Per-document access control**: Validate that the authenticated user is allowed to access the specific `[workspaceId, documentId]` key, not just any document. +- **Cap and rate limit `applyUpdate`**: Update payloads are unvalidated `number[]` arrays with no size limit, and the example client sends one action per keystroke and per cursor move with zero throttling. Enforce payload size caps and per-connection rate limits on the server, and debounce on the client. +- **Do not trust client-asserted awareness clientIds**: The `clientId` argument to `applyUpdate` is client-supplied and trusted as-is. Derive or verify presence identity from connection-scoped server state instead. +- **Destroy actors and KV on delete**: `deleteDocument` only filters the entry out of the coordinator's index. The document actor and its KV snapshot are orphaned. On delete, also destroy the document actor and its storage, with a permission check on who may delete. diff --git a/vendor/actors/docs/content/learn/cron-jobs.mdx b/vendor/actors/docs/content/learn/cron-jobs.mdx new file mode 100644 index 0000000..9d397f9 --- /dev/null +++ b/vendor/actors/docs/content/learn/cron-jobs.mdx @@ -0,0 +1,68 @@ +--- +title: "Cron Jobs and Scheduled Tasks" +description: "Patterns for durable one-shot, calendar, and fixed-interval work on Rivet Actors." +templates: ["scheduling"] +--- + +Rivet Actor schedules are durable actor-local timers. They survive actor sleep, restarts, upgrades, deploys, and crashes without a separate cron service. + +## Choose a schedule type + +| API | Use it for | +| --- | --- | +| `c.schedule.after(delayMs, action, ...args)` | One-time work after a relative delay. | +| `c.schedule.at(timestamp, action, ...args)` | One-time work at an exact Unix timestamp in milliseconds. | +| `c.cron.set({ ... })` | Named calendar recurrence in an IANA timezone. | +| `c.cron.every({ ... })` | Named fixed intervals of at least 5 seconds. | + +All callbacks are ordinary actions on the same actor. Keep the action name fixed in your code rather than accepting an arbitrary action name from a client. + +See [Schedule & Cron](/actors/docs/schedule) for the full API, history, cancellation, failure behavior, and limits. + +## Calendar job + +Use `cron.set` instead of manually re-arming a one-shot action: + + + +Install fixed background jobs in `onCreate` so setup runs once per actor. The job name remains an upsert key, so a later `cron.set` call updates the existing job rather than creating a duplicate. `cron.set` also handles timezone and daylight-saving transitions. + +## Fixed-interval job + +Use `cron.every` for frequent work such as presence sweeps or cache refreshes: + +```ts +await c.cron.every({ + name: "presence-sweep", + interval: 15_000, // Minimum 5 seconds. + action: "sweepPresence", + maxHistory: 25, +}); +``` + +Intervals remain anchored to scheduled deadlines rather than drifting by the action's runtime. If a previous run is still active, the overlapping occurrence is skipped. + +## Cancellation and updates + +Keep the ID returned by a one-shot schedule when it may need cancellation: + +```ts +const id = await c.schedule.after(60_000, "expireSession", sessionId); +await c.schedule.cancel(id); +``` + +Recurring jobs are managed by name: + +```ts +await c.cron.delete("presence-sweep"); +``` + +Calling `cron.set` or `cron.every` again with the same name replaces its configuration. + +## Failure and idempotency + +Keep scheduled actions idempotent when duplicate work would be harmful. See [Execution behavior](/actors/docs/schedule#execution-behavior) for retry behavior and workflow guidance. + +## Topology + +Use a singleton actor key for one global job, such as `jobs["daily-report"]`. Use an actor per user or resource for isolated reminders, trials, billing periods, or other per-entity schedules. diff --git a/vendor/actors/docs/content/learn/index.mdx b/vendor/actors/docs/content/learn/index.mdx new file mode 100644 index 0000000..ade6a1b --- /dev/null +++ b/vendor/actors/docs/content/learn/index.mdx @@ -0,0 +1,9 @@ +--- +title: "Learn" +description: "End-to-end guides for building with Rivet Actors." +skill: false +--- + + +**TODO.** Overview page listing the guides below. + diff --git a/vendor/actors/docs/content/learn/live-cursors.mdx b/vendor/actors/docs/content/learn/live-cursors.mdx new file mode 100644 index 0000000..03a763a --- /dev/null +++ b/vendor/actors/docs/content/learn/live-cursors.mdx @@ -0,0 +1,163 @@ +--- +title: "Live Cursors and Presence" +description: "Live cursors and multiplayer presence with Rivet Actors: per-connection cursor state, realtime updates over events or raw WebSockets, and throttling." +templates: ["cursors", "cursors-raw-websocket"] +--- + +Patterns for building live cursors, multiplayer presence, and realtime cursor sharing with RivetKit. One room actor fans cursor positions out to every connected client, keyed per room with [actor keys](/actors/docs/keys). + +## Starter Code + +Start with one of the two working variants on GitHub. Both implement the same collaborative cursor canvas with persistent text labels; they differ only in transport. + +| Variant | Starter Code | Transport | Presence Storage | +| --- | --- | --- | --- | +| `cursors` | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/cursors) | Typed [actions](/actors/docs/actions) and [events](/actors/docs/events) over the RivetKit connection | `connState` per connection | +| `cursors-raw-websocket` | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/cursors-raw-websocket) | Raw [`onWebSocket` handler](/actors/docs/websocket-handler) with a custom JSON message protocol | Socket map in `createVars` | + +Use `cursors` by default: typed actions, typed events, and automatic connection tracking cover most apps with less code. Use `cursors-raw-websocket` when you need full control of the wire format, for example a custom JSON or binary protocol, or clients that do not use the RivetKit client library. + +## Connection State vs Persistent State + +Presence is ephemeral by definition. A cursor position is only meaningful while its connection is alive, so it belongs in per-connection storage, not in persistent actor state. Persistent state is reserved for data that must survive disconnects and actor restarts. + +| Data | Where It Lives | Why | +| --- | --- | --- | +| Cursor position | `connState` (`cursors`) or the `createVars` socket map (`cursors-raw-websocket`) | Scoped to one connection and discarded with it. Stale presence cannot accumulate in storage. | +| Text labels (`textLabels`) | Persistent actor `state` in both variants | Canvas content must survive disconnects and actor restarts. | + +In the `cursors` variant, `updateCursor` writes `c.conn.state.cursor` and `getRoomState` rebuilds the presence snapshot by iterating `c.conns.values()`, so the cursor map is always derived from live connections rather than stored. See [Connections](/actors/docs/connections) for `connState` and [State](/actors/docs/state) for persistence semantics. + +## Presence Lifecycle + +- **Join**: The `cursors-raw-websocket` variant pushes an `init` message with the current `{ cursors, textLabels }` snapshot as soon as a socket connects. The `cursors` variant has no explicit join broadcast; the client calls the `getRoomState` action once after connecting to seed its local maps, and peers first see a new user on that user's first `cursorMoved` broadcast. +- **Move**: Every `updateCursor` call writes the connection's presence entry, then broadcasts `cursorMoved` to all connections, including the sender. +- **Leave**: The `cursors` variant handles leave in `onDisconnect`, broadcasting `cursorRemoved` with the connection's last cursor. The raw variant does the same from the socket `close` listener, then deletes the session from the `vars.websockets` map. Clients delete that user from their local cursor map, so stale cursors disappear the moment a tab closes. + +See [Lifecycle](/actors/docs/lifecycle) for `onDisconnect` and `createVars`. + +## Update Throttling + +Neither example throttles. Both frontends send a cursor update on every raw `mousemove` event with no debounce or interval cap. That is fine for a demo, but a fast mouse on a high-refresh display can emit hundreds of events per second per user. The patterns below are recommended production hardening on top of the starter code, not something the examples implement. + +| Layer | Pattern | Guidance | +| --- | --- | --- | +| Client (smoothness) | Throttle to 20-30Hz | Sample the latest pointer position every 33-50ms and send only that. Drop intermediate moves, but always flush the final position so cursors settle at the true location. Interpolate between received positions on the rendering side. | +| Server (enforcement) | Per-connection rate limit | Track the last accepted update timestamp per connection and drop or coalesce updates arriving faster than your cap. Client throttles are cooperative; the actor is the enforcement boundary. | + +## Actors + + + + +- **Key**: `cursorRoom[roomId]` (the frontend defaults `roomId` to `"general"`) +- **Responsibility**: Holds per-connection cursor presence in `connState`, persists shared text labels in actor state, and broadcasts cursor and text updates to all connections. +- **Actions** + - `updateCursor` + - `updateText` + - `removeText` + - `getRoomState` +- **Events** + - `cursorMoved` + - `cursorRemoved` + - `textUpdated` + - `textRemoved` +- **Queues** + - None +- **State** + - JSON + - `textLabels` (persistent) + - `connState.cursor` per connection (ephemeral) + + + + +- **Key**: `cursorRoom[roomId]` (resolved via `client.cursorRoom.getOrCreate(roomId)`) +- **Responsibility**: Exposes a raw WebSocket endpoint, tracks live sockets and their cursors in a `createVars` map keyed by a `sessionId` query parameter, persists text labels, and manually fans JSON frames out to every socket. +- **Actions** + - `getOrCreate` (stub returning `{ status: "ok" }`; the frontend resolves the actor ID with the client handle's `getOrCreate(roomId).resolve()`, which creates the actor without dispatching this action) + - `getRoomState` +- **Queues** + - None +- **State** + - JSON + - `textLabels` (persistent) + - `vars.websockets` map of `sessionId` to socket and cursor (in-memory, lost on restart) + + + + +The raw variant defines no RivetKit events. Its message names are `type` fields on raw JSON frames: + +| Direction | Message `type` | Payload | +| --- | --- | --- | +| Client to server | `updateCursor` | `{ userId, x, y }` | +| Client to server | `updateText` | `{ id, userId, text, x, y }` | +| Client to server | `removeText` | `{ id }` | +| Server to client | `init` | `{ cursors, textLabels }` snapshot on connect | +| Server to client | `cursorMoved`, `textUpdated`, `textRemoved`, `cursorRemoved` | The corresponding cursor, label, or ID payload | + +## Lifecycle + + + + +```mermaid +sequenceDiagram + participant A as Client A + participant R as cursorRoom + participant B as Other Clients + + A->>R: connect via useActor (cursorRoom[roomId]) + A->>R: getRoomState() + R-->>A: {cursors, textLabels} + loop every mouse move + A->>R: updateCursor(userId, x, y) + Note over R: write c.conn.state.cursor + R-->>B: cursorMoved (broadcast) + end + A->>R: updateText(id, userId, text, x, y) + Note over R: upsert persistent state.textLabels + R-->>B: textUpdated (broadcast) + Note over A: tab closes + Note over R: onDisconnect reads conn.state.cursor + R-->>B: cursorRemoved (broadcast) +``` + + + + +```mermaid +sequenceDiagram + participant A as Client A + participant R as cursorRoom + participant B as Other Clients + + A->>R: getOrCreate(roomId).resolve() + R-->>A: actorId + A->>R: open WebSocket /gateway/{actorId}/websocket?sessionId=... + Note over R: close 1008 if sessionId is missing + Note over R: store socket in vars.websockets + R-->>A: init {cursors, textLabels} + loop every mouse move + A->>R: {type: "updateCursor"} frame + Note over R: update session cursor in vars + R-->>B: cursorMoved frame + end + Note over A: socket closes + R-->>B: cursorRemoved frame + Note over R: delete session from vars.websockets +``` + + + + +## Security Checklist + +Both examples ship without authentication so the presence pattern stays readable. Everything below is recommended hardening for production, not behavior the examples implement. + +- **Identity**: Bind presence identity to the connection (`c.conn.id` in the actions variant, a server-generated session ID in the raw variant). Never trust a client-supplied `userId`; in the examples it is a random client-generated string, so any client can impersonate or remove any cursor. +- **Authorization**: Authorize label mutations by owner. In the examples, `updateText` accepts arbitrary `id` and `userId` arguments and `removeText` accepts an arbitrary `id`, so any client can edit or delete any label. +- **Input validation**: Clamp `x` and `y` to canvas bounds, cap text label length, and cap the total `textLabels` count so persistent state cannot grow unbounded. +- **Rate limiting**: Enforce a per-connection cap on `updateCursor` (for example 30Hz) and on label writes, as described in [Update Throttling](#update-throttling). +- **Protocol strictness (raw variant)**: Validate message shape before use and close the socket on malformed JSON instead of logging and continuing. Reject duplicate `sessionId` values rather than silently overwriting another session's socket entry. diff --git a/vendor/actors/docs/content/learn/multiplayer-game.mdx b/vendor/actors/docs/content/learn/multiplayer-game.mdx new file mode 100644 index 0000000..b04f45b --- /dev/null +++ b/vendor/actors/docs/content/learn/multiplayer-game.mdx @@ -0,0 +1,796 @@ +--- +title: "Multiplayer Game" +description: "Pragmatic patterns for building multiplayer games: matchmaking, tick loops, realtime state, interest management, and validation." +templates: ["multiplayer-game-patterns"] +--- + +Patterns for building multiplayer games with RivetKit, intended as a practical checklist you can adapt per genre. + +## Starter Code + +Start with one of the working examples on [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/) and adapt it to your game. Do not start from scratch for matchmaking and lifecycle flows. + +| Game Classification | Starter Code | Common Examples | +| --- | --- | --- | +| Battle Royale | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/battle-royale/) | Fortnite, Apex Legends, PUBG, Warzone | +| Arena | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/arena/) | Call of Duty TDM/FFA, Halo Slayer, Counter-Strike casual, VALORANT unrated, Overwatch Quick Play, Rocket League | +| IO Style | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/io-style/) | Agar.io, Slither.io, surviv.io | +| Open World | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/open-world/) | Minecraft survival servers, Rust-like worlds, MMO zone/chunk worlds | +| Party | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/party/) | Fall Guys private lobbies, custom game rooms, social party sessions | +| Physics 2D | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/physics-2d/) | Top-down physics brawlers, 2D arena games, platform fighters | +| Physics 3D | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/physics-3d/) | Physics sandbox sessions, 3D arena games, movement playgrounds | +| Ranked | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/ranked/) | Chess ladders, competitive card games, duel arena ranked queues | +| Turn-Based | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/turn-based/) | Chess correspondence, Words With Friends, async board games | +| Idle | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/multiplayer-game-patterns/src/actors/idle/) | Cookie Clicker, Idle Miner Tycoon, Adventure Capitalist | + +## Server Simulation + +### Game Loop And Tick Rates + +| 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 + +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). + +Pick one engine per simulation. Keep frontend-only libs out of backend simulation paths and treat server state as authoritative. + +| 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/) | + +### Spatial Indexing + +For non-physics spatial queries, use a dedicated index instead of naive `O(n^2)` checks: + +| Index Type | Recommendation | +| --- | --- | +| 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 + +| Model | When To Use | Implementation | +| --- | --- | --- | +| 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 + +- **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 + +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. + + +### Interest Management + +Control what each client receives to reduce bandwidth and prevent information leaks. + +#### Per-Player Replication Filters + +- **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 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 + +- **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 Patterns + +Common building blocks used across the architecture patterns below. + +#### Actor Topology + +| Primitive | Use When | Typical Ownership | +| --- | --- | --- | +| `matchmaker["main"]` + `match[matchId]` | Session-based multiplayer (battle royale, arena, ranked, party, turn-based) | Matchmaker owns discovery/assignment. Match owns lifecycle and gameplay state. | +| `chunk[worldId,chunkX,chunkY]` | Large continuous worlds that need sharding | Each chunk owns local players, chunk state, and local simulation. | +| `world[playerId]` | Per-player progression loops (idle/solo world state) | Per-player resources, buildings, timers, and progression. | +| `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. | + +#### Queueing Strategy + +- 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 + +Start with this baseline, then harden further for competitive or high-risk environments. + +### Baseline Checklist + +- **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 + +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. +- Enforce world bounds (and basic collision if applicable). +- Rate limit update frequency (for example 20Hz max). + +## Architecture Patterns + +Each game type below starts with a quick summary table, then details actors and lifecycle. + +### Battle Royale + +| Topic | Summary | +| --- | --- | +| Matchmaking | Immediate routing to the fullest non-started lobby (oldest tie-break); players wait in lobby until capacity, then the match starts. | +| Netcode | Hybrid. Client owns movement, camera, and local prediction. Server owns zone state, projectiles, hit resolution, eliminations, loot, and final placement. | +| Tick Rate | 10 ticks/sec (`100ms`) with a fixed loop for zone progression and lifecycle checks. | +| Physics | Client owns movement with server anti-cheat validation; projectiles, hits, and damage are server-authoritative. Use `@dimforge/rapier3d` for 3D or `@dimforge/rapier2d` for top-down 2D. | + +**Actors** + + + + +- **Key**: `matchmaker["main"]` +- **Responsibility**: Finds or creates lobbies, tracks pending reservations, and maintains occupancy. +- **Actions** + - `findMatch` + - `pendingPlayerConnected` + - `updateMatch` + - `closeMatch` +- **Queues** + - `findMatch` + - `pendingPlayerConnected` + - `updateMatch` + - `closeMatch` +- **State** + - SQLite + - `matches` + - `pending_players` + - `player_count` includes connected and pending players + + + + +- **Key**: `match[matchId]` +- **Responsibility**: Runs lobby/live/finished phases, owns player state, zone progression, and eliminations. +- **Actions** + - `connect` + - Movement and combat actions +- **Queues** + - None +- **State** + - JSON + - `phase` + - `players` + - `zone` + - `eliminations` + - `snapshot data` + + + + +**Lifecycle** + +```mermaid +sequenceDiagram + participant C as Client + participant MM as matchmaker + participant M as match + + C->>MM: findMatch() + alt no open lobby + MM->>M: create(matchId) + end + MM-->>C: {matchId, playerId} + C->>M: connect(playerId) + M->>MM: pendingPlayerConnected(matchId, playerId) + MM-->>M: accepted + Note over M: lobby countdown -> live + M-->>C: snapshot + shoot events + M->>MM: closeMatch(matchId) +``` + +### Arena + +| Topic | Summary | +| --- | --- | +| Matchmaking | Mode-based fixed-capacity queues (`duo`, `squad`, `ffa`) that build only full matches and pre-assign teams (except FFA). | +| Netcode | Hybrid. Client owns movement plus prediction and smoothing. Server owns team or FFA assignment, projectiles, hit resolution, phase transitions, and scoring. | +| Tick Rate | 20 ticks/sec (`50ms`) with a tighter loop for live team and FFA snapshots. | +| Physics | Medium to high intensity; client movement with server validation and server-authoritative combat/entities. | + +**Actors** + + + + +- **Key**: `matchmaker["main"]` +- **Responsibility**: Runs mode queues, builds full matches, assigns teams, and publishes assignments. +- **Actions** + - `queueForMatch` + - `unqueueForMatch` + - `matchCompleted` +- **Queues** + - `queueForMatch` + - `unqueueForMatch` + - `matchCompleted` +- **State** + - SQLite + - `player_pool` + - `matches` + - `assignments` keyed by connection and player + + + + +- **Key**: `match[matchId]` +- **Responsibility**: Runs match phases and in-match player/team state for score and win conditions. +- **Actions** + - `connect` + - Gameplay actions +- **Queues** + - None +- **State** + - JSON + - `phase` + - `players` + - `team assignments` + - `score and win state` + + + + +**Lifecycle** + +```mermaid +sequenceDiagram + participant C as Client + participant MM as matchmaker + participant M as match + + C->>MM: queueForMatch(mode) + Note over MM: enqueue in player_pool + Note over MM: fill when capacity reached + MM->>M: create(matchId, assignments) + Note over MM: persist assignments + MM-->>C: assignmentReady + C->>M: connect(playerId) + Note over M: waiting -> live when all players connect + M->>MM: matchCompleted(matchId) +``` + +### IO Style + +| Topic | Summary | +| --- | --- | +| Matchmaking | Open-lobby routing to the fullest room below capacity; room counts are heartbeated and new lobbies are auto-created when needed. | +| Netcode | Server-authoritative with interpolation. Client sends input intents and interpolates. Server owns movement, bounds, room membership, and canonical snapshots. | +| Tick Rate | 10 ticks/sec (`100ms`) with lightweight periodic room snapshots. | +| Physics | Low to medium intensity; server-authoritative kinematic movement, escalating to a physics engine only when collisions get complex. | + +**Actors** + + + + +- **Key**: `matchmaker["main"]` +- **Responsibility**: Routes players into the fullest open lobby and tracks reservations and occupancy. +- **Actions** + - `findLobby` + - `pendingPlayerConnected` + - `updateMatch` + - `closeMatch` +- **Queues** + - `findLobby` + - `pendingPlayerConnected` + - `updateMatch` + - `closeMatch` +- **State** + - SQLite + - `matches` + - `pending_players` + - Occupancy includes pending reservations + + + + +- **Key**: `match[matchId]` +- **Responsibility**: Runs per-match movement simulation and broadcasts snapshots. +- **Actions** + - `connect` + - `setInput` +- **Queues** + - None +- **State** + - JSON + - `players` + - `inputs` + - `movement state` + - `snapshot cache` + + + + +**Lifecycle** + +```mermaid +sequenceDiagram + participant C as Client + participant MM as matchmaker + participant M as match + + C->>MM: findLobby() + alt no open lobby + MM->>M: create(matchId) + end + MM-->>C: {matchId, playerId} + C->>M: connect(playerId) + M->>MM: pendingPlayerConnected(matchId, playerId) + MM-->>M: accepted + Note over M: fixed tick simulation + M-->>C: snapshot events + M->>MM: closeMatch(matchId) +``` + +### Open World + +| Topic | Summary | +| --- | --- | +| Matchmaking | Client-driven chunk routing from world coordinates, with nearby chunk windows preloaded via adjacent chunk connections. | +| Netcode | Hybrid for sandbox (client movement with validation) or server-authoritative for MMO-like flows. Server owns chunk routing, persistence, and canonical world state. | +| Tick Rate | 10 ticks/sec per chunk actor (`100ms`), so load scales with active chunks. | +| Physics | Medium to high at scale; chunk-local simulation can be server-authoritative (MMO-like) or client movement with server validation (sandbox-like). | + +**Actors** + + + + +- **Key**: `chunk[worldId,chunkX,chunkY]` +- **Responsibility**: Owns chunk-local players, blocks, movement tick, and chunk membership. +- **Actions** + - `connect` + - `enterChunk` + - `addPlayer` + - `setInput` + - `leaveChunk` + - `removePlayer` +- **Queues** + - None +- **State** + - JSON + - `connections` + - `players` + - `blocks` scoped to one chunk key + + + + +**Lifecycle** + +```mermaid +sequenceDiagram + participant C as Client + participant CH as chunk + + Note over C: resolve chunk keys from world position + loop each visible chunk + C->>CH: connect(worldId, chunkX, chunkY, playerId) + Note over CH: store connection metadata + end + C->>CH: enterChunk/addPlayer + loop movement updates + C->>CH: setInput(...) + CH-->>C: snapshot + end + C->>CH: leaveChunk/removePlayer or disconnect + Note over CH: remove membership and metadata +``` + +### Party + +| Topic | Summary | +| --- | --- | +| Matchmaking | Host-created private party flow using party codes and explicit joins. | +| Netcode | Server-authoritative (basic logic). Server owns membership, host permissions, and phase transitions. | +| Tick Rate | No continuous tick; updates are event-driven (`join`, `start`, `finish`). | +| Physics | Low intensity for lobby-first flows; usually no dedicated physics or indexing unless you add realtime mini-games. | + +**Actors** + + + + +- **Key**: `matchmaker["main"]` +- **Responsibility**: Handles party create/join flow, validates join tickets, and tracks party size. +- **Actions** + - `createParty` + - `joinParty` + - `verifyJoin` + - `updatePartySize` + - `closeParty` +- **Queues** + - `createParty` + - `joinParty` + - `verifyJoin` + - `updatePartySize` + - `closeParty` +- **State** + - SQLite + - `parties` + - `join_tickets` for party lookup and join validation + + + + +- **Key**: `match[matchId]` +- **Responsibility**: Owns party members, host role, ready flags, and phase transitions. +- **Actions** + - `connect` + - `startGame` + - `finishGame` +- **Queues** + - None +- **State** + - JSON + - `members` + - `host` + - `ready state` + - `phase` + - `party events` + + + + +**Lifecycle** + + + + +```mermaid +sequenceDiagram + participant H as Host Client + participant MM as matchmaker + participant M as match + + H->>MM: createParty() + MM-->>H: {matchId, partyCode, playerId, joinToken} + H->>M: connect(playerId, joinToken) + M->>MM: verifyJoin(...) + MM-->>M: allowed + M->>MM: updatePartySize(playerCount) + H->>M: startGame() / finishGame() + M->>MM: closeParty(matchId) +``` + + + + +```mermaid +sequenceDiagram + participant J as Joiner Client + participant MM as matchmaker + participant M as match + + J->>MM: joinParty(partyCode) + MM-->>J: {matchId, playerId, joinToken} + J->>M: connect(playerId, joinToken) + M->>MM: verifyJoin(...) + MM-->>M: allowed / denied + M->>MM: updatePartySize(playerCount) +``` + + + + +### Ranked + +| Topic | Summary | +| --- | --- | +| Matchmaking | ELO-based queue pairing with a widening search window as wait time increases. | +| Netcode | Hybrid. Client owns movement with local prediction and interpolation. Server owns projectiles, hit resolution, match results, and rating updates. | +| Tick Rate | 20 ticks/sec (`50ms`) with fixed live ticks for deterministic pacing and broadcast cadence. | +| Physics | Medium to high intensity; client movement with server validation and server-authoritative combat/hit resolution. | + +**Actors** + + + + +- **Key**: `matchmaker["main"]` +- **Responsibility**: Runs rating-based queueing, pairing, assignment persistence, and completion fanout. +- **Actions** + - `queueForMatch` + - `unqueueForMatch` + - `matchCompleted` +- **Queues** + - `queueForMatch` + - `unqueueForMatch` + - `matchCompleted` +- **State** + - SQLite + - `player_pool` + - `matches` + - `assignments` with rating window and connection scoping + + + + +- **Key**: `match[matchId]` +- **Responsibility**: Runs ranked match phase, score, and winner reporting. +- **Actions** + - `connect` + - Gameplay actions +- **Queues** + - None +- **State** + - JSON + - `phase` + - `players` + - `score` + - `winner` + - `completion payload` + + + + +- **Key**: `player[username]` +- **Responsibility**: Stores canonical player MMR and win/loss profile. +- **Actions** + - `initialize` + - `getRating` + - `applyMatchResult` +- **Queues** + - None +- **State** + - JSON + - `rating` + - `wins` + - `losses` + - `match counters` + + + + +- **Key**: `leaderboard["main"]` +- **Responsibility**: Stores and serves top-ranked players. +- **Actions** + - `updatePlayer` +- **Queues** + - None +- **State** + - SQLite + - Leaderboard score rows + - Top-list ordering + + + + +**Lifecycle** + +```mermaid +sequenceDiagram + participant C as Client + participant MM as matchmaker + participant P as player + participant M as match + participant LB as leaderboard + + C->>MM: queueForMatch(username) + MM->>P: initialize/getRating + P-->>MM: rating + Note over MM: store queue row + retry pairing + MM->>M: create(matchId, assigned players) + MM-->>C: assignmentReady + C->>M: connect(username) + M->>MM: matchCompleted(...) + MM->>P: applyMatchResult(...) + MM->>LB: updatePlayer(...) + Note over MM: remove matches + assignments rows +``` + +### Turn-Based + +| Topic | Summary | +| --- | --- | +| Matchmaking | Async private-invite and public-queue pairing in the same pattern. | +| Netcode | Server-authoritative (basic logic). Client can draft moves before submit. Server owns turn ownership, committed move log, turn order, and completion state. | +| Tick Rate | No continuous tick; move submission and turn transitions drive updates. | +| Physics | Very low intensity; no realtime physics loop, just discrete rules validation. Indexing is optional and mostly for board or query convenience at scale. | + +**Actors** + + + + +- **Key**: `matchmaker["main"]` +- **Responsibility**: Handles private invite and public queue pairing for async matches. +- **Actions** + - `createGame` + - `joinByCode` + - `queueForMatch` + - `unqueueForMatch` + - `closeMatch` +- **Queues** + - `createGame` + - `joinByCode` + - `queueForMatch` + - `unqueueForMatch` + - `closeMatch` +- **State** + - SQLite + - `matches` + - `player_pool` + - `assignments` for invite and queue mapping + + + + +- **Key**: `match[matchId]` +- **Responsibility**: Owns board state, turn order, move validation, and final result. +- **Actions** + - `connect` + - `makeMove` +- **Queues** + - None +- **State** + - JSON + - `board` + - `turns` + - `players` + - `connection presence` + - `result` + + + + +**Lifecycle** + + + + +```mermaid +sequenceDiagram + participant A as Client A + participant B as Client B + participant MM as matchmaker + participant M as match + + A->>MM: queueForMatch() + B->>MM: queueForMatch() + Note over MM: pair first two queued players + MM->>M: create(matchId) + seed X/O players + MM-->>A: assignment/match info + MM-->>B: assignment/match info + A->>M: connect(playerId) + B->>M: connect(playerId) + A->>M: makeMove() + B->>M: makeMove() + opt all players disconnected for timeout + Note over M: destroy after idle timeout + end + M->>MM: closeMatch(matchId) +``` + + + + +```mermaid +sequenceDiagram + participant A as Client A + participant B as Client B + participant MM as matchmaker + participant M as match + + A->>MM: createGame() + MM-->>A: {matchId, playerId, inviteCode} + B->>MM: joinByCode(inviteCode) + MM->>M: create(matchId) + seed X/O players + MM-->>A: assignment/match info + MM-->>B: assignment/match info + A->>M: connect(playerId) + B->>M: connect(playerId) + A->>M: makeMove() + B->>M: makeMove() + M->>MM: closeMatch(matchId) +``` + + + + +### Idle + +| Topic | Summary | +| --- | --- | +| Matchmaking | No matchmaker; each player uses a direct per-player actor and a shared leaderboard actor. | +| Netcode | Server-authoritative (basic logic). Client owns UI and build intent. Server owns resources, production rates, building validation, and leaderboard totals. | +| Tick Rate | No continuous tick; use `c.schedule.after(...)` for coarse intervals and compute offline catch-up from elapsed wall time. | +| Physics | None for standard idle loops; transitions are discrete (`build`, `collect`, `upgrade`) and do not need spatial indexing. | + +**Actors** + + + + +- **Key**: `world[playerId]` +- **Responsibility**: Owns one player's progression, buildings, production scheduling, and state updates. +- **Actions** + - `initialize` + - `build` + - `collectProduction` +- **Queues** + - None +- **State** + - JSON + - Per-player buildings + - `resources` + - `timers` + - `progression state` + + + + +- **Key**: `leaderboard["main"]` +- **Responsibility**: Stores global scores and serves leaderboard updates. +- **Actions** + - `updateScore` +- **Queues** + - `updateScore` +- **State** + - SQLite + - `scores` table keyed by player + - Current leaderboard totals + + + + +**Lifecycle** + +```mermaid +sequenceDiagram + participant C as Client + participant W as world + participant LB as leaderboard + + C->>W: getOrCreate(playerId) + initialize() + Note over W: seed state + schedule collection + W-->>C: stateUpdate + loop gameplay loop + C->>W: build() / collectProduction() + W->>LB: updateScore(...) + Note over LB: upsert scores + LB-->>C: leaderboardUpdate + W-->>C: stateUpdate + end +``` diff --git a/vendor/actors/docs/content/learn/per-tenant-database.mdx b/vendor/actors/docs/content/learn/per-tenant-database.mdx new file mode 100644 index 0000000..37d9c52 --- /dev/null +++ b/vendor/actors/docs/content/learn/per-tenant-database.mdx @@ -0,0 +1,122 @@ +--- +title: "Database per Tenant" +description: "Multi-tenant data isolation with one Rivet Actor per tenant: the actor key is the tenant id, so each tenant gets its own isolated dataset and migrations." +templates: ["per-tenant-database"] +--- + +Patterns for database-per-tenant architectures with RivetKit. Instead of one shared database with a `tenant_id` column on every table, each tenant gets its own Rivet Actor, and that actor owns the tenant's entire dataset. + +## Starter Code + +Start with the working example on [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/per-tenant-database) and adapt it. The example stores each tenant's dataset in JSON actor state and serves a React dashboard with live event updates. + +| Topic | Summary | +| --- | --- | +| Isolation | One `companyDatabase` actor per tenant, keyed by company name. Switching tenants swaps the entire dataset. | +| State | JSON actor state holding `employees` and `projects` arrays plus timestamps. No SQLite, no queues, no scheduling. | +| Realtime | Every write action mutates state, then broadcasts a typed event (`employeeAdded`, `projectAdded`) to all connected clients of that tenant. | +| Auth | None. The sign-in screen is cosmetic. Production guidance is in the [security checklist](#security-checklist). | + +## The Isolation Model + +The actor key is the tenant id. The client connects with `useActor({ name: "companyDatabase", key: [companyName] })` and the actor reads `c.key[0]` in `createState` to seed that tenant's dataset. This gives you: + +- **One actor per tenant**: `companyDatabase[tenantId]` addresses exactly one actor instance. Two tenants can never share an actor. +- **One dataset per tenant**: All reads and writes go through that actor's [state](/actors/docs/state), so there is no shared table with a `tenant_id` column to filter incorrectly. Cross-tenant leaks require constructing the wrong key, not forgetting a `WHERE` clause. +- **No key injection**: Keys are arrays, not interpolated strings. `key: [tenantId]` cannot be escaped the way `"tenant:" + tenantId` string concatenation can. See [Keys](/actors/docs/keys). + +The example's test ([tests/per-tenant-database.test.ts](https://github.com/rivet-dev/rivet/tree/main/examples/per-tenant-database/tests/per-tenant-database.test.ts)) proves the isolation: data written to `companyDatabase["Alpha Co"]` never appears in `companyDatabase["Beta Co"]`. + +## Choosing a State Backend + +The example uses plain JSON actor state. The same key-equals-tenant model works with any actor state backend. + +| Backend | Use When | Docs | Working Code | +| --- | --- | --- | --- | +| JSON actor state | Small datasets, simple reads, whole dataset fits comfortably in memory. What the example uses. | [State](/actors/docs/state) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/per-tenant-database) | +| Actor SQLite (`rivetkit/db`) | Tables, indexes, SQL queries, larger-than-memory data, per-tenant relational schema. | [SQLite](/actors/docs/sqlite) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/kitchen-sink/src/actors/state/sqlite-raw.ts) | +| SQLite + Drizzle | Typed schema, query builder, and generated migration files on top of actor SQLite. | [SQLite + Drizzle](/actors/docs/sqlite-drizzle) | [GitHub](https://github.com/rivet-dev/rivet/tree/main/examples/kitchen-sink/src/actors/state/sqlite-drizzle/) | + +With either SQLite option, every tenant gets its own embedded SQLite database, since the database is scoped to the actor and the actor is scoped to the tenant. + +## Migrations + +The per-tenant example has no migrations because JSON state has no schema. When you adopt SQLite, migrations run per tenant database: + +- **Raw SQL**: `db({ onMigrate })` runs your migration SQL inside a SQLite savepoint before the actor serves traffic. If `onMigrate` throws, all migration SQL rolls back atomically and the actor does not start. See [SQLite](/actors/docs/sqlite). +- **Drizzle**: `drizzle-kit` generates migration files from your typed schema, and `db({ schema, migrations })` applies them when the actor wakes. See [SQLite + Drizzle](/actors/docs/sqlite-drizzle). + +Because each tenant has its own database, migrations roll out per actor as each tenant's actor wakes, rather than as one large migration against a shared database. + +## Tenant Id Must Come From Auth + +The example's sign-in is cosmetic: the client picks any company string and that string becomes the actor key, so any visitor can read and write any tenant's data. Do not ship this. As a required production extension (not implemented by the example): + +- Derive the tenant id from a verified credential, such as a JWT claim, never from user input. +- Validate the credential against `c.key` in `onBeforeConnect` (pass/fail) or `createConnState` (store the verified user on connection state). See [Authentication](/actors/docs/authentication) and [Connections](/actors/docs/connections). +- Add per-action permission checks on top of connection-level auth. See [Access Control](/actors/docs/access-control). + +## Actors + + + + +- **Key**: `companyDatabase[companyName]` (single-element array key; `c.key[0]` is the company name) +- **Responsibility**: One actor per tenant. Holds that company's employees and projects in persistent state, serves reads and writes via actions, and broadcasts mutations to connected clients. +- **Actions** + - `addEmployee` + - `listEmployees` + - `addProject` + - `listProjects` + - `getStats` +- **Queues** + - None +- **Events** + - `employeeAdded` + - `projectAdded` +- **State** + - JSON + - `company_name` + - `employees` + - `projects` + - `created_at` + - `updated_at` + + + + +Every write action follows the same mutate-then-broadcast shape: push the record into `c.state`, bump `updated_at`, broadcast the typed event, return the record. See [Actions](/actors/docs/actions) and [Events](/actors/docs/events). + +## Lifecycle + +```mermaid +sequenceDiagram + participant A as Tenant A client + participant DA as companyDatabase A + participant B as Tenant B client + participant DB as companyDatabase B + + Note over A: authenticate and derive tenant id + A->>DA: connect with key [tenantA] + Note over DA: createState seeds company_name, employees, projects + A->>DA: listEmployees() + listProjects() + getStats() + A->>DA: addEmployee(name, role) + DA-->>A: employeeAdded event + B->>DB: connect with key [tenantB] + Note over DB: separate actor, separate dataset + B->>DB: listEmployees() + DB-->>B: tenant B data only +``` + +In the example, the "authenticate" step is a free-text company picker. The rest of the flow matches the diagram: `createState` seeds the dataset on first creation, the dashboard loads with `listEmployees`, `listProjects`, and `getStats`, and every connected client of the same tenant receives `employeeAdded` and `projectAdded` events. + +## Security Checklist + +The example ships with none of these. Apply all of them before production. + +- **Tenant identity**: Derive the tenant id from a verified JWT claim, never from a client-supplied string. +- **Connection validation**: In `onBeforeConnect` or `createConnState`, verify the credential's tenant claim matches `c.key` and reject mismatches. +- **Per-action authorization**: Check the caller's role before mutating actions (`addEmployee`, `addProject`), not just at connect time. See [Access Control](/actors/docs/access-control). +- **Input validation**: Clamp name and role lengths and validate enums. The example only trims input and substitutes fallback defaults. +- **Key construction**: Always pass the tenant id as an array element (`key: [tenantId]`). Never interpolate tenant ids into key strings, and never build keys from one tenant's input to address another tenant's actor. +- **Growth limits**: As a recommended extension, cap or paginate the `employees` and `projects` arrays. The example lets them grow unboundedly in JSON state; move to [SQLite](/actors/docs/sqlite) when the dataset outgrows memory. diff --git a/vendor/actors/docs/content/tutorials/ai-agent.mdx b/vendor/actors/docs/content/tutorials/ai-agent.mdx deleted file mode 100644 index 4d104fc..0000000 --- a/vendor/actors/docs/content/tutorials/ai-agent.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: "Build an AI Agent" -description: "Build a stateful AI agent on top of a Rivet Actor." -skill: false ---- - - -**TODO.** Migrate and rewrite the AI agent recipe as a step-by-step tutorial. - -**Source material:** `website/src/content/cookbook/ai-agent.mdx` - diff --git a/vendor/actors/docs/content/tutorials/chat-room.mdx b/vendor/actors/docs/content/tutorials/chat-room.mdx deleted file mode 100644 index 33e6d66..0000000 --- a/vendor/actors/docs/content/tutorials/chat-room.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: "Build a Chat Room" -description: "Build a realtime chat room backed by a Rivet Actor." -skill: false ---- - - -**TODO.** Migrate and rewrite the chat room recipe as a step-by-step tutorial. - -**Source material:** `website/src/content/cookbook/chat-room.mdx` - diff --git a/vendor/actors/docs/content/tutorials/index.mdx b/vendor/actors/docs/content/tutorials/index.mdx deleted file mode 100644 index 8332b6b..0000000 --- a/vendor/actors/docs/content/tutorials/index.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: "Tutorials" -description: "End-to-end guides for building with Rivet Actors." -skill: false ---- - - -**TODO.** Overview page listing the actor tutorials. The existing cookbook recipes are the starting set and need to be migrated into this tab. - -**Source material:** `website/src/content/cookbook/` - diff --git a/vendor/actors/docs/content/use-cases/index.mdx b/vendor/actors/docs/content/use-cases/index.mdx new file mode 100644 index 0000000..9fa3440 --- /dev/null +++ b/vendor/actors/docs/content/use-cases/index.mdx @@ -0,0 +1,9 @@ +--- +title: "Use Cases" +description: "What people build with Rivet Actors." +--- + + +**TODO.** Overview of what Rivet Actors is used for, one section per use case, each +linking to the guide that shows how to build it. + diff --git a/vendor/actors/docs/sidebar.json b/vendor/actors/docs/sidebar.json index 8c99499..d3feabc 100644 --- a/vendor/actors/docs/sidebar.json +++ b/vendor/actors/docs/sidebar.json @@ -216,6 +216,10 @@ { "title": "Limits", "href": "/actors/docs/limits" + }, + { + "title": "Regions & Multi-Region", + "href": "/actors/docs/general/edge" } ] } @@ -311,6 +315,10 @@ { "title": "Pool Configuration", "href": "/actors/docs/general/pool-configuration" + }, + { + "title": "Container Runner", + "href": "/actors/docs/container-runner" } ] }, @@ -318,11 +326,6 @@ "title": "API Reference", "collapsible": true, "pages": [ - { - "title": "TypeScript API", - "href": "/typedoc", - "external": true - }, { "title": "OpenAPI", "href": "https://github.com/rivet-dev/rivet/tree/main/rivetkit-openapi", @@ -342,72 +345,91 @@ { "title": "Skill File", "href": "/actors/docs/general/skill" - }, - { - "title": "Docs for LLMs", - "href": "/actors/docs/general/docs-for-llms" } ] } ] } ], - "tutorials": [ + "learn": [ { "title": "General", "pages": [ { "title": "Overview", - "href": "/actors/tutorials", + "href": "/actors/learn", "icon": "faSquareInfo" } ] }, { - "title": "Guides", + "title": "Architecture", "pages": [ { - "title": "Build a Chat Room", - "href": "/actors/tutorials/chat-room", - "badge": "TODO" - }, - { - "title": "Build an AI Agent", - "href": "/actors/tutorials/ai-agent", - "badge": "TODO" + "title": "A Radically Simpler Architecture", + "href": "/actors/learn/a-radically-simpler-architecture" } ] }, { - "title": "To Migrate", + "title": "Guides", "pages": [ { - "title": "Chat Room", - "href": "/cookbook/chat-room" + "title": "AI Agent", + "href": "/actors/learn/ai-agent" }, { - "title": "AI Agent", - "href": "/cookbook/ai-agent" + "title": "Chat Room", + "href": "/actors/learn/chat-room" }, { "title": "Collaborative Text Editor", - "href": "/cookbook/collaborative-text-editor" + "href": "/actors/learn/collaborative-text-editor" + }, + { + "title": "Cron Jobs and Scheduled Tasks", + "href": "/actors/learn/cron-jobs" }, { - "title": "Cron Jobs", - "href": "/cookbook/cron-jobs" + "title": "Database per Tenant", + "href": "/actors/learn/per-tenant-database" }, { - "title": "Live Cursors", - "href": "/cookbook/live-cursors" + "title": "Live Cursors and Presence", + "href": "/actors/learn/live-cursors" }, { "title": "Multiplayer Game", - "href": "/cookbook/multiplayer-game" + "href": "/actors/learn/multiplayer-game" + } + ] + } + ], + "integrations": [ + { + "title": "General", + "pages": [ + { + "title": "Overview", + "href": "/actors/integrations", + "icon": "faSquareInfo" + } + ] + }, + { + "title": "Integrations", + "pages": [ + { + "title": "Flue", + "href": "/actors/integrations/flue" + }, + { + "title": "Vercel Eve", + "href": "/actors/integrations/vercel-eve" }, { - "title": "Per-Tenant Database", - "href": "/cookbook/per-tenant-database" + "title": "Vercel Workflows (Beta)", + "href": "/actors/integrations/vercel-workflows" } ] } diff --git a/vendor/actors/engine/artifacts/config-schema.json b/vendor/actors/engine/artifacts/config-schema.json index ead08e9..9147b7a 100644 --- a/vendor/actors/engine/artifacts/config-schema.json +++ b/vendor/actors/engine/artifacts/config-schema.json @@ -40,17 +40,6 @@ } ] }, - "api_public": { - "default": null, - "anyOf": [ - { - "$ref": "#/definitions/ApiPublic" - }, - { - "type": "null" - } - ] - }, "auth": { "default": null, "anyOf": [ @@ -214,27 +203,6 @@ }, "additionalProperties": false }, - "ApiPublic": { - "description": "Configuration for the public API service.", - "type": "object", - "properties": { - "respect_forwarded_for": { - "description": "Flag to respect the X-Forwarded-For header for client IP addresses.\n\nWill be ignored in favor of CF-Connecting-IP if DNS provider is configured as Cloudflare.", - "type": [ - "boolean", - "null" - ] - }, - "verbose_errors": { - "description": "Flag to enable verbose error reporting.", - "type": [ - "boolean", - "null" - ] - } - }, - "additionalProperties": false - }, "Auth": { "type": "object", "required": [ @@ -855,6 +823,24 @@ ], "format": "int64" }, + "actor_create_rate_limit_drip_rate_ms": { + "description": "Time to regain one actor creation token per namespace.\n\nUnit is in milliseconds.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "actor_create_rate_limit_requests": { + "description": "Max burst of actor creations per namespace before throttling.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, "actor_retry_duration_threshold": { "description": "How long to wait after starting to attempt to reallocate before before setting actor to sleep.\n\nUnit is in milliseconds.", "type": [ @@ -986,6 +972,24 @@ "format": "uint64", "minimum": 0.0 }, + "envoy_websocket_rate_limit_drip_rate_us": { + "description": "Time to regain one inbound WebSocket message token on a single envoy connection.\n\nUnit is in microseconds. The envoy connection multiplexes every actor on a runner, so the sustained ceiling is far higher than the per-client gateway limit and needs sub-millisecond granularity to express.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "envoy_websocket_rate_limit_requests": { + "description": "Max burst of inbound WebSocket messages on a single envoy connection before throttling.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, "gateway_gc_interval_ms": { "description": "GC interval for in-flight requests in milliseconds.", "type": [ @@ -1057,6 +1061,24 @@ "format": "uint64", "minimum": 0.0 }, + "gateway_websocket_rate_limit_drip_rate_ms": { + "description": "Time to regain one inbound WebSocket message token on a single connection.\n\nUnit is in milliseconds.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "gateway_websocket_rate_limit_requests": { + "description": "Max burst of inbound WebSocket messages on a single connection before throttling.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, "hibernating_request_eligible_threshold": { "description": "How long after last ping before considering a hibernating request disconnected.\n\nUnit is in milliseconds.", "type": [ @@ -1222,47 +1244,18 @@ "url" ], "properties": { - "ssl": { - "description": "SSL configuration options", + "nats": { + "description": "NATS configuration for UniversalDB multi-node mode.\n\nWhen set, UniversalDB runs in multi-node mode and uses NATS for follower-to-leader commit transport instead of an in-process resolver. When absent, UniversalDB runs single-node. If unset but the UPS pubsub is configured for NATS, this is inherited from that config at startup (see `Root::validate_and_set_defaults`).", "default": null, "anyOf": [ { - "$ref": "#/definitions/PostgresSsl" + "$ref": "#/definitions/Nats" }, { "type": "null" } ] }, - "url": { - "description": "URL to connect to Postgres with\n\nSupports standard PostgreSQL connection parameters including `sslmode`. Supported sslmode values: `disable`, `prefer` (default), `require`. To verify server certificates, use `sslmode=require` with `ssl.root_cert_path`.\n\nExample with sslmode: `postgresql://user:pass@host:5432/db?sslmode=require`\n\nSee: https://docs.rs/postgres/0.19.10/postgres/config/struct.Config.html#url", - "allOf": [ - { - "$ref": "#/definitions/Secret" - } - ] - } - }, - "additionalProperties": false - }, - "Postgres2": { - "type": "object", - "required": [ - "url" - ], - "properties": { - "disable_memory_optimization": { - "description": "When true, force every UPS publish to round-trip through the postgres driver instead of taking the in-process fast path for subjects that have a local subscriber on the same engine pod. Opt-in diagnostic; default false.", - "default": false, - "type": "boolean" - }, - "memory_optimization": { - "deprecated": true, - "type": [ - "boolean", - "null" - ] - }, "ssl": { "description": "SSL configuration options", "default": null, @@ -1276,7 +1269,7 @@ ] }, "url": { - "description": "URL to connect to Postgres with\n\nSupports standard PostgreSQL connection parameters including `sslmode`. Supported sslmode values: `disable`, `prefer` (default), `require`. To verify server certificates, use `sslmode=require` with `ssl.root_cert_path`.\n\nSee: https://docs.rs/postgres/0.19.10/postgres/config/struct.Config.html#url", + "description": "URL to connect to Postgres with\n\nSupports standard PostgreSQL connection parameters including `sslmode`. Supported sslmode values: `disable`, `prefer` (default), `require`. To verify server certificates, use `sslmode=require` with `ssl.root_cert_path`.\n\nExample with sslmode: `postgresql://user:pass@host:5432/db?sslmode=require`\n\nSee: https://docs.rs/postgres/0.19.10/postgres/config/struct.Config.html#url", "allOf": [ { "$ref": "#/definitions/Secret" diff --git a/vendor/actors/self-host/control-plane/kubernetes/12-postgres-statefulset.yaml b/vendor/actors/self-host/control-plane/kubernetes/12-postgres-statefulset.yaml index e1e14a4..c035d26 100644 --- a/vendor/actors/self-host/control-plane/kubernetes/12-postgres-statefulset.yaml +++ b/vendor/actors/self-host/control-plane/kubernetes/12-postgres-statefulset.yaml @@ -20,7 +20,7 @@ spec: spec: containers: - name: postgres - image: postgres:17 + image: postgres:18 args: - postgres - -c