rivet / rivetkit-typescript
rivet-dev/rivet/rivetkit-typescript/CLAUDE.md
CLAUDE.md6.3k starsChanged 6 months ago
What's in it
- rivetkit-typescript/CLAUDE.md
- Inspector UI Embed (two-step build)
- Pre-Rust Refactor Reference
- Tree-Shaking Boundaries
- Runtime Boundary
- Native SQLite v2
- Context Types Sync
- Gateway Targets
- Raw KV Limits
- Startup Logging
- NAPI Receive Loop
- Sleep Shutdown
- Drizzle Compatibility Testing
- Test Harness
- Cloudflare Workers Compatibility
- Wasm Binding Package
- Workflow Context Actor Access Guards
- Dynamic Actors Architecture Doc
# rivetkit-typescript/CLAUDE.md
## Inspector UI Embed (two-step build)
- The inspector UI is embedded into the `rivetkit-napi` / `rivetkit-wasm` native binaries by `rivetkit-core`'s `build.rs`, but it's built downstream of them, so a direct turbo `dependsOn` would cycle. A scoped `build:embed` task re-runs the native build after `build:inspector-ui`; `pnpm -w build` (= `turbo build build:embed`) handles it. Any partial build that runs `@rivetkit/rivetkit-napi#build` without `build:embed` leaves the `.node` with an empty bundle and `/inspector/ui/*` returns `ui_asset_not_found` 404. Fix and full context: [Build troubleshooting](../.claude/reference/build-troubleshooting.md#inspector-ui-embed-empty-bundle--ui_asset_not_found-404).
## Pre-Rust Refactor Reference
- Commit `1761e6f0f` (`chore: rivetkit to rust`, 2026-04-16) is the cutover from the all-TypeScript runtime to the Rust core + NAPI bindings. Use `1761e6f0f^` (or earlier) when comparing current behavior against the original TypeScript implementation, e.g. for lifecycle ordering, persistence layout, or any "what did the TS version do here?" question.
## Tree-Shaking Boundaries
- Do not import `@rivetkit/workflow-engine` outside the `rivetkit/workflow` entrypoint so it remains tree-shakeable.
- Keep SQLite runtime code on the native `@rivetkit/rivetkit-napi` path. Do not reintroduce WebAssembly SQLite or KV-backed VFS fallbacks.
- Importing `rivetkit/db` is the explicit opt-in for SQLite. Do not lazily load extra SQLite runtimes from that entrypoint.
- Core drivers must remain SQLite-agnostic. Any SQLite-specific wiring belongs behind the native database provider boundary.
- Before deleting a `rivetkit/*` package export, grep `examples/`, `website/`, and `frontend/` for self-imports. Those consumers are part of the supported package surface on this branch.
## Runtime Boundary
- Select runtime behavior from `CoreRuntime.kind`, not `instanceof` adapter classes; NAPI maps to the native runtime kind and wasm maps to wasm.
- Keep `CoreRuntime` SQL methods on the portable `RuntimeSql*` structs from `packages/rivetkit/src/registry/runtime.ts`; NAPI-only `Buffer` conversion belongs inside `NapiCoreRuntime`.
- Keep `RuntimeSqlBindParam` variants exact and `RuntimeSqlExecuteResult.route` limited to `read`, `write`, or `writeFallback`; normalize generated adapter output before returning it.
- Keep `CoreRuntime` byte payloads on `RuntimeBytes`/`Uint8Array`; NAPI-only `Buffer` conversion belongs inside `NapiCoreRuntime`.
- Shared actor glue in `packages/rivetkit/src/registry/native.ts` should construct `RuntimeBytes`/`Uint8Array`; leave `Buffer` creation to `NapiCoreRuntime`.
- Wasm bindings for NAPI-supported runtime APIs should forward to `rivetkit-core`; avoid placeholder returns that break runtime parity.
- Wasm `registerTask` must forward to core `ActorContext::register_task(...)`, not `waitUntil`, so shutdown-drain semantics match NAPI without conflating public `waitUntil` work.
- Bridge error HTTP status promotion belongs in `rivetkit-core`; TS native/wasm runtime adapters should decode encoded status fields instead of maintaining promotion tables.
- Use public `sqlite` config for runtime SQLite backend selection; wasm defaults unset SQLite to remote and must reject local before runtime construction.
## Native SQLite v2
- If `packages/rivetkit` still needs a BARE codec after schema-generator removal, vendor only the live generated modules under `src/common/bare/` and import them from source instead of `dist/schemas/**`.
- The v2 SQLite VFS must reconstruct full 4 KiB pages for partial `xRead` and `xWrite` callbacks because SQLite can issue sub-page header I/O even when commits stay page-based.
- Treat `head_txid` and `db_size_pages` as VFS-owned state. Read-side `get_pages(...)` responses may refresh `max_delta_bytes`, but commit responses plus local `xWrite` or `xTruncate` paths are the only things allowed to advance or shrink those fields.
- When changing Rust under `packages/rivetkit-napi` or `packages/sqlite-native`, rebuild from `packages/rivetkit-napi` with `pnpm build:force` so the native `.node` artifact refreshes.
- Real `sqlite-native` tests that drive the v2 VFS through a direct `SqliteEngine` need a multithread Tokio runtime; `current_thread` is fine for mock transport tests but can stall real engine callbacks.
- Treat any sqlite v2 transport or commit error as fatal for that VFS instance: mark it dead, surface it through `take_last_kv_error()`, and rely on reopen plus takeover instead of trying to limp forward with dirty pages still buffered.
- Keep sqlite v2 fatal commit cleanup in `flush_dirty_pages` and `commit_atomic_write`; callback wrappers should only translate fence mismatches into SQLite I/O return codes.
- If the native SQLite layer exposes new introspection or metrics getters, forward them through `wrapJsNativeDatabase(...)` or actor inspector metrics will silently lose that data.
## Context Types Sync
- Keep the `*ContextOf` types exported from `packages/rivetkit/src/actor/contexts/index.ts` and re-exported by `packages/rivetkit/src/actor/mod.ts` in sync with the two docs locations below when adding, removing, or renaming context types.
- `website/src/content/docs/actors/types.mdx` — public docs page
- `website/src/content/docs/actors/index.mdx` — crash course (Context Types section)
## Gateway Targets
For client-facing gateway operations, use the shared `GatewayTarget` type from `packages/rivetkit/src/engine-client/driver.ts` instead of ad hoc `string | ActorQuery` unions. The engine control client should preserve direct actor ID behavior and resolve `ActorQuery` targets inside the client implementation so higher-level client flows can widen their target type without duplicating query-resolution logic.
Keep the TypeScript `ActorKey` and `ActorContext.key` surfaces string-only unless `client/query.ts`, key serialization, and gateway query parsing are widened end to end in the same change.
Actor-connect protocol `actionId` values are nullable, and `0` is a valid action ID. Treat only `null` as a connection-level error.
Query-backed remote gateway URLs use `rvt-*` query parameters: `/gateway/{name}/{path}?rvt-namespace=...&rvt-method=...&rvt-key=...`. The actor name is a clean path segment, and all routing params are standard query parameters with the `rvt-` prefix. The known `rvt-*` params are: `rvt-namespace`, `rvt-method`, `rvt-runner`, `rvt-key`, `rvt-input`, `rvt-region`, `rvt-crash-policy`, `rvt-token`. `rvt-runner` is required for `getOrCreate` and disallowed for `get`. For multi-component keys, use a single comma-separated `rvt-key` param (e.g. `rvt-key=tenant,room`). Use `URLSearchParams` to build and parse query strings.
Keep `buildGatewayUrl()` query-backed for `get()` and `getOrCreate()` handles instead of pre-resolving to an actor ID. Local `getGatewayUrl()` flows should exercise the shared `actorGateway` query param parser on the served runtime router path, while direct actor ID targets still use `/gateway/{actorId}`.
When parsing query gateway paths in `packages/rivetkit/src/actor-gateway/gateway.ts` or in parity implementations, detect query paths by checking if any query parameter starts with `rvt-`. Use `URLSearchParams` to parse query params. Partition into `rvt-*` params and actor params. Reject raw `@token` syntax, unknown `rvt-*` params, and duplicate scalar `rvt-*` params. Strip all `rvt-*` params from the query string before forwarding to the actor by reconstructing the query string from only the actor params.
Once a query path has been parsed in `packages/rivetkit/src/actor-gateway/gateway.ts`, resolve it to an actor ID inside the shared path-based HTTP and WebSocket gateway helpers before calling `proxyRequest` or `proxyWebSocket`. After resolution, reuse the existing direct-ID proxy flow and preserve the original remaining path with `rvt-*` params stripped.
When adding or validating query input payloads in `packages/rivetkit/src/engine-client/actor-websocket-client.ts`, enforce `ClientConfig.maxInputSize` against the raw CBOR byte length before base64url encoding. This keeps the limit aligned with the actual serialized payload instead of the encoded URL expansion.
For `ClientRaw.get()` and `ClientRaw.getOrCreate()` flows, do not cache a resolved actor ID on `ActorResolutionState`. Key-based handles and connections should resolve fresh for each operation so they do not stay pinned to an older actor selection after a destroy or recreate.
For gateway-facing client helpers in `packages/rivetkit/src/client`, derive the `EngineControlClient` target from `getGatewayTarget()` instead of calling `resolveActorId()` up front. `get()` and `getOrCreate()` handles must pass their `ActorQuery` through to `sendRequest`, `openWebSocket`, and `buildGatewayUrl` so each request and reconnect re-resolves at the gateway. Only `getForId()` and create-backed handles should collapse to a plain actor ID target.
## Raw KV Limits
- Always enforce engine limits when working with raw actor KV.
- Max key size: 2048 bytes.
- Max batch payload size (`kv put`): 976 KiB total across keys + values.
- Max entries per batch (`kv put`): 128 key-value pairs.
- Max total actor KV storage: 10 GiB.
- Design raw KV operations to handle these constraints, and split operations into multiple requests if a per-request limit can be exceeded.
- Treat the total actor KV storage limit (10 GiB) as a hard limit, and fail closed with explicit errors instead of swallowing, truncating, or ignoring KV write failures.
- Update `website/src/content/docs/actors/limits.mdx` in the same change when KV, queue, workflow persistence, SQLite-over-KV, or any limit-related actor behavior changes.
## Startup Logging
Every discrete phase of actor startup must have a corresponding debug log. Use the prefix `perf internal:` for framework/infrastructure phases and `perf user:` for user-code callbacks. For example:
```
DEBUG perf internal: loadStateMs durationMs=...
DEBUG perf internal: initQueueMs durationMs=...
DEBUG perf user: onCreateMs durationMs=...
DEBUG perf user: dbMigrateMs durationMs=...
```
The log name matches the key in `ActorMetrics.startup`. Internal phases use `perf internal:`, user-code callbacks use `perf user:`. This convention keeps startup logs greppable and makes it easy to separate framework overhead from user-code time. When adding a new startup phase, always add a corresponding log with the appropriate prefix and update the `#userStartupKeys` set in `ActorInstance` if the phase runs user code.
## NAPI Receive Loop
- Keep adapter-owned long-lived task handles (for example the NAPI `run` handler) in `packages/rivetkit-napi/src/napi_actor_events.rs` and expose only sync restart hooks through shared `ActorContext` state; JS-facing restart methods must not depend on async locks.
- Do not mirror actor `ready`/`started` flags in `packages/rivetkit-napi/src/actor_context.rs`; read and write those lifecycle gates through core `ActorContext` so sleep gating has a single source of truth.
- Graceful adapter drains in `packages/rivetkit-napi/src/napi_actor_events.rs` should use `while let Some(...) = tasks.join_next().await`; `JoinSet::shutdown()` aborts in-flight work and breaks Sleep/Destroy ordering.
- `Sleep` and `Destroy` must set the shared adapter `end_reason` on both success and error replies; otherwise the outer receive loop keeps consuming queued events after shutdown has already failed.
- On this branch, the native TS actor/conn persistence glue still lives in `packages/rivetkit/src/registry/native.ts`; PRD references to split `state-manager.ts` or `connection-manager.ts` files may be stale, so land equivalent behavior in `registry/native.ts` unless those modules reappear first.
- Public TS actor `onWake` maps to the native callback bag's `onWake`; `onBeforeActorStart` is an internal driver/NAPI startup hook, not public actor config.
- Static actor `state` values in `packages/rivetkit/src/registry/native.ts` must be `structuredClone(...)`d per actor instance; reusing the literal leaks mutations across different keyed actors.
- JS-only native actor caches in `packages/rivetkit/src/registry/native.ts` should live on `ActorContext.runtimeState()`, not on actorId-keyed module globals. Same-key recreates must get a fresh bag.
- Every `NativeConnAdapter` construction path in `packages/rivetkit/src/registry/native.ts` must keep the `CONN_STATE_MANAGER_SYMBOL` hookup; hibernatable conn mutations rely on core `ConnHandle::set_state` dirty tracking to request persistence.
- Durable native actor saves in `packages/rivetkit/src/registry/native.ts` must use `ctx.requestSaveAndWait({ immediate: true })`; state bytes are collected only through the `serializeState` callback.
- Opaque user payloads that must preserve JS `undefined` across Rust JSON/CBOR bridges should go through `encodeCborCompat` / `decodeCborCompat`; do not use those helpers on structural JSON envelopes where omitted fields must stay omitted.
- Reply-bearing TSF dispatches in `packages/rivetkit-napi/src/napi_actor_events.rs` must wrap the callback future in `with_timeout(...)` via a shared timed-spawn helper; raw `spawn_reply(...)` on HTTP or workflow callbacks can leak stuck JS promises until shutdown.
- Dispatch cancellation between `packages/rivetkit-napi/src/napi_actor_events.rs` and `packages/rivetkit/src/registry/native.ts` must pass `CancellationToken` objects end to end. Do not reintroduce BigInt token registries or polling loops.
- Native persistence and `saveState` coverage belongs in driver tests that use real NAPI plus `hardCrashActor` or sleep/wake. Do not mock `NativeActorContext` in Vitest for that path.
## Sleep Shutdown
- Sleep shutdown should wait for in-flight HTTP action work and pending disconnect callbacks before `onSleep`, but should not block on open hibernatable connections alone because existing connection actions may still complete during the graceful shutdown window.
- Driver tests that wait for a target actor to sleep should record lifecycle events into a separate observer actor; do not poll target actions or hold a normal target connection open while waiting.
## Drizzle Compatibility Testing
To test rivetkit's drizzle integration against multiple drizzle-orm versions:
```bash
cd rivetkit-typescript/packages/rivetkit
./scripts/test-drizzle-compat.sh # test all default versions
./scripts/test-drizzle-compat.sh 0.44.2 0.45.1 # test specific versions
```
The script installs each drizzle-orm version, typechecks `scripts/drizzle-compat-smoke.ts` against the `rivetkit/db/drizzle` public surface, and reports pass/fail per version. It restores the original package.json and lockfile on exit. Update the `DEFAULT_VERSIONS` array in the script when adding support for new drizzle releases.
## Test Harness
- Shared local `rivet-engine` lifecycle for TypeScript tests lives in `packages/rivetkit/tests/shared-engine.ts`; driver and platform tests should reuse it instead of launching a separate engine.
- Runtime parity tests can instantiate `NapiCoreRuntime` and `WasmCoreRuntime` with fake binding classes, then drive shared actor glue through `buildNativeFactory(...)` without requiring generated NAPI or wasm artifacts.
- Platform wasm smoke tests should reuse `packages/rivetkit/tests/platforms/shared-registry.ts` for the raw-SQL SQLite counter actor and public wasm `setup(...)` shape.
- Platform smoke tests should use `packages/rivetkit/tests/platforms/shared-platform-harness.ts` for serverless runner setup, app process logging, temp app dirs, health checks, and pinned `pnpm dlx` CLI launches.
- When adding, removing, or renaming a `packages/rivetkit/tests/driver/*.test.ts` file that uses `describeDriverMatrix`, update the Fast/Slow/Excluded test lists and the suite-description table in `.claude/skills/driver-test-runner/SKILL.md` in the same change.
## Cloudflare Workers Compatibility
Cloudflare Workers forbid `setTimeout`, `fetch`, `connect`, and other async I/O in global scope (outside a request handler). The `Registry` constructor runs in global scope, so it must never call these APIs unconditionally. Any deferred work (e.g., prestarting the runtime) must be gated behind a synchronous config check before scheduling a timer. See `packages/rivetkit/src/registry/index.ts` for the pattern: the outer `if` guards `setTimeout`, and the inner `if` re-checks after the tick to pick up late config mutations.
## Wasm Binding Package
- Treat `packages/rivetkit-wasm/pkg/` as wasm-pack output; commit source and build scripts, then regenerate package artifacts during package builds.
- Export wasm raw WebSocket handles as `WebSocketHandle`, not `WebSocket`, because wasm-bindgen rejects classes that shadow the host global.
- Keep wasm runtime adapter byte normalization on `Uint8Array`; do not add Node `Buffer` dependencies to `packages/rivetkit/src/registry/wasm-runtime.ts`.
- Pass platform wasm bindings through `setup({ wasm: { bindings, initInput } })`; do not add hidden `globalThis` binding hooks.
- Wasm `CoreRegistry` serverless startup must use a `BuildingServerless` waiter state; shutdown during build must wake waiters and drain any newly built runtime.
- Validate wasm-only serve config constraints before converting to core `ServeConfig` or starting registry side effects.
- Run `pnpm --filter @rivetkit/rivetkit-wasm run check:package` after wasm package export or files changes to verify the published tarball includes the root entrypoint and wasm artifacts.
- Build `@rivetkit/rivetkit-wasm` with its package-local pinned `wasm-pack` dependency. Do not use `npx -y wasm-pack`.
- Intern wasm bridged `RivetErrorSchema` values by `(group, code)` only; live per-error messages must stay on `RivetError.message` instead of expanding the schema cache key.
- Track wasm websocket callback regions in a map keyed by active region IDs and remove entries on end so repeated callbacks do not retain empty slots.
- Treat wasm websocket callback region ID `0` as the untracked sentinel; wrap `u32` allocation by skipping live IDs instead of panicking.
## Workflow Context Actor Access Guards
- Workflow contexts are split in two (`packages/rivetkit/src/workflow/context.ts`): `WorkflowContext` (workflow fn + `try`/`loop`/`race`/`join` callbacks) exposes only replayable primitives and no actor data; `WorkflowStepContext` (`step`/`tryStep`/`rollback` callbacks) is the only place actor data and side effects are reachable.
- Side-effectful and actor-data members belong on `WorkflowStepContext` and must call `#ensureActive` so a step context captured and used after its step settles throws; only read-only props (for example `actorId`, `name`, `key`, `log`, `abortSignal`) are exempt.
- Do not add actor data (`state`, `vars`, `db`, `client`, `broadcast`) to `WorkflowContext`; reach it through the step context passed to `step`/`tryStep`.
## Dynamic Actors Architecture Doc
- Reference `docs-internal/rivetkit-typescript/DYNAMIC_ACTORS_ARCHITECTURE.md` when working on dynamic actor behavior, bridge contracts, isolate lifecycle, or runtime sandbox wiring.
- Keep `docs-internal/rivetkit-typescript/DYNAMIC_ACTORS_ARCHITECTURE.md` up to date in the same change whenever dynamic actor architecture, lifecycle, bridge payloads, security behavior, or temporary compatibility paths change.
More agent context in rivet-dev/rivet
20 other files this repository gives its agents.
AGENTS.md
CLAUDE.md
Skill
- create-launch-post.claude/skills/create-launch-post/SKILL.md
- creating-docs-diagrams.claude/skills/creating-docs-diagrams/SKILL.md
- driver-test-runner.claude/skills/driver-test-runner/SKILL.md
- sanity-check.claude/skills/sanity-check/SKILL.md
- stack-merge.claude/skills/stack-merge/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
No reports yet. Be the first to say whether it worked.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool registry_write, action report. How to connect one.

