- The inspector UI is embedded into the
rivetkit-napi/rivetkit-wasmnative binaries byrivetkit-core'sbuild.rs, but it's built downstream of them, so a direct turbodependsOnwould cycle. A scopedbuild:embedtask re-runs the native build afterbuild:inspector-ui;pnpm -w build(=turbo build build:embed) handles it. Any partial build that runs@rivetkit/rivetkit-napi#buildwithoutbuild:embedleaves the.nodewith an empty bundle and/inspector/ui/*returnsui_asset_not_found404. Fix and full context: Build troubleshooting.
- Commit
1761e6f0f(chore: rivetkit to rust, 2026-04-16) is the cutover from the all-TypeScript runtime to the Rust core + NAPI bindings. Use1761e6f0f^(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.
- Do not import
@rivetkit/workflow-engineoutside therivetkit/workflowentrypoint so it remains tree-shakeable. - Keep SQLite runtime code on the native
@rivetkit/rivetkit-napipath. Do not reintroduce WebAssembly SQLite or KV-backed VFS fallbacks. - Importing
rivetkit/dbis 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, grepexamples/,website/, andfrontend/for self-imports. Those consumers are part of the supported package surface on this branch.
- Select runtime behavior from
CoreRuntime.kind, notinstanceofadapter classes; NAPI maps to the native runtime kind and wasm maps to wasm. - Keep
CoreRuntimeSQL methods on the portableRuntimeSql*structs frompackages/rivetkit/src/registry/runtime.ts; NAPI-onlyBufferconversion belongs insideNapiCoreRuntime. - Keep
RuntimeSqlBindParamvariants exact andRuntimeSqlExecuteResult.routelimited toread,write, orwriteFallback; normalize generated adapter output before returning it. - Keep
CoreRuntimebyte payloads onRuntimeBytes/Uint8Array; NAPI-onlyBufferconversion belongs insideNapiCoreRuntime. - Shared actor glue in
packages/rivetkit/src/registry/native.tsshould constructRuntimeBytes/Uint8Array; leaveBuffercreation toNapiCoreRuntime. - Wasm bindings for NAPI-supported runtime APIs should forward to
rivetkit-core; avoid placeholder returns that break runtime parity. - Wasm
registerTaskmust forward to coreActorContext::register_task(...), notwaitUntil, so shutdown-drain semantics match NAPI without conflating publicwaitUntilwork. - 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
sqliteconfig for runtime SQLite backend selection; wasm defaults unset SQLite to remote and must reject local before runtime construction.
- If
packages/rivetkitstill needs a BARE codec after schema-generator removal, vendor only the live generated modules undersrc/common/bare/and import them from source instead ofdist/schemas/**. - The v2 SQLite VFS must reconstruct full 4 KiB pages for partial
xReadandxWritecallbacks because SQLite can issue sub-page header I/O even when commits stay page-based. - Treat
head_txidanddb_size_pagesas VFS-owned state. Read-sideget_pages(...)responses may refreshmax_delta_bytes, but commit responses plus localxWriteorxTruncatepaths are the only things allowed to advance or shrink those fields. - When changing Rust under
packages/rivetkit-napiorpackages/sqlite-native, rebuild frompackages/rivetkit-napiwithpnpm build:forceso the native.nodeartifact refreshes. - Real
sqlite-nativetests that drive the v2 VFS through a directSqliteEngineneed a multithread Tokio runtime;current_threadis 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_pagesandcommit_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.
-
Keep the
*ContextOftypes exported frompackages/rivetkit/src/actor/contexts/index.tsand re-exported bypackages/rivetkit/src/actor/mod.tsin 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)
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.
-
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.mdxin the same change when KV, queue, workflow persistence, SQLite-over-KV, or any limit-related actor behavior changes.
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.
- Keep adapter-owned long-lived task handles (for example the NAPI
runhandler) inpackages/rivetkit-napi/src/napi_actor_events.rsand expose only sync restart hooks through sharedActorContextstate; JS-facing restart methods must not depend on async locks. - Do not mirror actor
ready/startedflags inpackages/rivetkit-napi/src/actor_context.rs; read and write those lifecycle gates through coreActorContextso sleep gating has a single source of truth. - Graceful adapter drains in
packages/rivetkit-napi/src/napi_actor_events.rsshould usewhile let Some(...) = tasks.join_next().await;JoinSet::shutdown()aborts in-flight work and breaks Sleep/Destroy ordering. SleepandDestroymust set the shared adapterend_reasonon 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 splitstate-manager.tsorconnection-manager.tsfiles may be stale, so land equivalent behavior inregistry/native.tsunless those modules reappear first. - Public TS actor
onWakemaps to the native callback bag'sonWake;onBeforeActorStartis an internal driver/NAPI startup hook, not public actor config. - Static actor
statevalues inpackages/rivetkit/src/registry/native.tsmust bestructuredClone(...)d per actor instance; reusing the literal leaks mutations across different keyed actors. - JS-only native actor caches in
packages/rivetkit/src/registry/native.tsshould live onActorContext.runtimeState(), not on actorId-keyed module globals. Same-key recreates must get a fresh bag. - Every
NativeConnAdapterconstruction path inpackages/rivetkit/src/registry/native.tsmust keep theCONN_STATE_MANAGER_SYMBOLhookup; hibernatable conn mutations rely on coreConnHandle::set_statedirty tracking to request persistence. - Durable native actor saves in
packages/rivetkit/src/registry/native.tsmust usectx.requestSaveAndWait({ immediate: true }); state bytes are collected only through theserializeStatecallback. - Opaque user payloads that must preserve JS
undefinedacross Rust JSON/CBOR bridges should go throughencodeCborCompat/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.rsmust wrap the callback future inwith_timeout(...)via a shared timed-spawn helper; rawspawn_reply(...)on HTTP or workflow callbacks can leak stuck JS promises until shutdown. - Dispatch cancellation between
packages/rivetkit-napi/src/napi_actor_events.rsandpackages/rivetkit/src/registry/native.tsmust passCancellationTokenobjects end to end. Do not reintroduce BigInt token registries or polling loops. - Native persistence and
saveStatecoverage belongs in driver tests that use real NAPI plushardCrashActoror sleep/wake. Do not mockNativeActorContextin Vitest for that path.
- 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.
To test rivetkit's drizzle integration against multiple drizzle-orm versions:
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 versionsThe 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.
- Shared local
rivet-enginelifecycle for TypeScript tests lives inpackages/rivetkit/tests/shared-engine.ts; driver and platform tests should reuse it instead of launching a separate engine. - Runtime parity tests can instantiate
NapiCoreRuntimeandWasmCoreRuntimewith fake binding classes, then drive shared actor glue throughbuildNativeFactory(...)without requiring generated NAPI or wasm artifacts. - Platform wasm smoke tests should reuse
packages/rivetkit/tests/platforms/shared-registry.tsfor the raw-SQL SQLite counter actor and public wasmsetup(...)shape. - Platform smoke tests should use
packages/rivetkit/tests/platforms/shared-platform-harness.tsfor serverless runner setup, app process logging, temp app dirs, health checks, and pinnedpnpm dlxCLI launches. - When adding, removing, or renaming a
packages/rivetkit/tests/driver/*.test.tsfile that usesdescribeDriverMatrix, update the Fast/Slow/Excluded test lists and the suite-description table in.claude/skills/driver-test-runner/SKILL.mdin the same change.
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.
- 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, notWebSocket, because wasm-bindgen rejects classes that shadow the host global. - Keep wasm runtime adapter byte normalization on
Uint8Array; do not add NodeBufferdependencies topackages/rivetkit/src/registry/wasm-runtime.ts. - Pass platform wasm bindings through
setup({ wasm: { bindings, initInput } }); do not add hiddenglobalThisbinding hooks. - Wasm
CoreRegistryserverless startup must use aBuildingServerlesswaiter state; shutdown during build must wake waiters and drain any newly built runtime. - Validate wasm-only serve config constraints before converting to core
ServeConfigor starting registry side effects. - Run
pnpm --filter @rivetkit/rivetkit-wasm run check:packageafter wasm package export or files changes to verify the published tarball includes the root entrypoint and wasm artifacts. - Build
@rivetkit/rivetkit-wasmwith its package-local pinnedwasm-packdependency. Do not usenpx -y wasm-pack. - Intern wasm bridged
RivetErrorSchemavalues by(group, code)only; live per-error messages must stay onRivetError.messageinstead 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
0as the untracked sentinel; wrapu32allocation by skipping live IDs instead of panicking.
- Workflow contexts are split in two (
packages/rivetkit/src/workflow/context.ts):WorkflowContext(workflow fn +try/loop/race/joincallbacks) exposes only replayable primitives and no actor data;WorkflowStepContext(step/tryStep/rollbackcallbacks) is the only place actor data and side effects are reachable. - Side-effectful and actor-data members belong on
WorkflowStepContextand must call#ensureActiveso a step context captured and used after its step settles throws; only read-only props (for exampleactorId,name,key,log,abortSignal) are exempt. - Do not add actor data (
state,vars,db,client,broadcast) toWorkflowContext; reach it through the step context passed tostep/tryStep.
- Reference
docs-internal/rivetkit-typescript/DYNAMIC_ACTORS_ARCHITECTURE.mdwhen working on dynamic actor behavior, bridge contracts, isolate lifecycle, or runtime sandbox wiring. - Keep
docs-internal/rivetkit-typescript/DYNAMIC_ACTORS_ARCHITECTURE.mdup to date in the same change whenever dynamic actor architecture, lifecycle, bridge payloads, security behavior, or temporary compatibility paths change.