Internal wiring reference for rivetkit-rust/packages/rivetkit-core/. These are facts about the current implementation. For the principles that govern how new code is added, see the root CLAUDE.md layer + fail-by-default sections. For state-mutation semantics, see docs-internal/engine/rivetkit-core-state-management.md.
Actor subsystems are composed into ActorContextInner, not separate managers.
- Queue storage lives on
ActorContextInner. Behavior sits inactor/queue.rsimpl ActorContextblocks. Do not reintroduceArc<QueueInner>or a publicQueuere-export. - Connection storage lives on
ActorContextInner. Behavior sits inactor/connection.rsimpl ActorContextblocks. Do not reintroduceArc<ConnectionManagerInner>or a publicConnectionManagerre-export. - Actor state storage lives on
ActorContextInner. Behavior sits inactor/state.rsimpl ActorContextblocks. Do not reintroduceArc<ActorStateInner>or a publicActorStatere-export. - Schedule storage lives on
ActorContextInner. Behavior sits inactor/schedule.rsimpl ActorContextblocks. Do not reintroduceArc<ScheduleInner>or a publicSchedulere-export. - Event fanout lives directly in
ActorContext::broadcast. Do not reintroduce a separateEventBroadcastersubsystem.
Runtime actor persistence lives in internal SQLite tables. The _rivet_ table prefix is reserved for RivetKit runtime data and must not be used by user schemas.
_rivet_meta is a tiny bootstrap root, not a general-purpose runtime KV store. It is created before the numbered migration ladder, so schema_version cannot live in a table created by that ladder. Its other key, kv_import_state, deliberately survives clear_imported_storage so startup can detect an interrupted legacy import, clear partial destination rows, and retry safely. Generic text accessors for this table are limited to that migration bookkeeping.
| Table | Contents |
|---|---|
_rivet_meta |
Bootstrap and import bookkeeping key-value rows such as schema_version and kv_import_state |
_rivet_actor |
Cold actor startup fields such as has_initialized and input |
_rivet_actor_state |
Hot serialized user state |
_rivet_schedule_events |
Durable scheduled actions |
_rivet_conns / _rivet_conn_state |
Hibernatable websocket cold metadata and hot state |
_rivet_runtime |
Runtime singletons: last pushed alarm, inspector token, queue next id |
_rivet_queue |
Queue messages |
_rivet_wf_kv |
TypeScript workflow storage with verbatim packed keys |
_rivet_user_kv |
Deprecated user c.kv compatibility storage |
The first wake on the migrated runtime runs actor/migrate_kv_to_sqlite/ before actor readiness. The importer live-scans the legacy actor KV keyspace, copies recognized records into SQLite, marks _rivet_meta.kv_import_state = done, and leaves legacy KV bytes untouched as a frozen downgrade snapshot.
Legacy traces under [7, 1, ...] are intentionally skipped and are not copied to _rivet_user_kv. The only runtime KV write after migration is the inspector token mirror at [3], kept until the dashboard stops fetching the token through the public actor-KV endpoint.
request_saveusesRequestSaveOpts { immediate, max_wait_ms }. NAPI callers usectx.requestSave({ immediate, maxWaitMs }). Do not use a booleanrequestSaveorrequestSaveWithin.- Receive-loop persistence routes deferred saves through
ActorContext::request_save(...)+ActorEvent::SerializeState { reason: Save, .. }. - Shutdown adapters persist explicitly with
ActorContext::save_state(Vec<StateDelta>)becauseSleep/Destroyreplies are unit-only. Direct durability must still clear pending save-request flags after a successful write. - Actor state is post-boot delta-only. Use
request_save/save_state(Vec<StateDelta>). Do not reintroduceset_state/mutate_state. - Schedule mutations write point rows in
_rivet_schedule_events, then resync the envoy alarm to the earliest event. - State mutations from inside
on_state_changecallbacks fail withactor/state_mutation_reentrant. Use vars or another non-state side channel for callback-run counters.
- Live inspector state rides
ActorContext::inspector_attach()returning anInspectorAttachGuardplussubscribe_inspector(). Hold the guard for the websocket lifetime soActorTaskcan debounceSerializeState { reason: Inspector, .. }off request-save hooks. - Cross-cutting inspector hooks stay anchored on
ActorContext. Queue-specific callbacks carry the current size; connection updates read the context connection count so unconfigured inspectors stay cheap no-ops.
Schedulealarm sync is guarded bydirty_since_push. Fresh schedules start dirty, mutations set dirty, and unchanged shutdown syncs must not re-push identical envoy alarms.- Persisted driver-alarm dedup stores the last pushed
Option<i64>in_rivet_runtime.last_pushed_alarm. Startup loads it from SQLite and skips identical future alarm pushes.
- HTTP and WebSocket staging helpers keep transport failures at the boundary.
on_requesterrors become HTTP 500 responses;on_websocketerrors become logged 1011 closes.ConnHandleandWebSocketwrappers surface explicit configuration errors through internaltry_*helpers. - Bulk transport disconnect helpers sweep every matching connection, remove the successful disconnects, update connection/sleep bookkeeping, then aggregate any per-connection failures into the returned error.
- Receive-loop
ActorEvent::Actiondispatch usesconn: Nonefor alarm-originated work andSome(ConnHandle)for real client connections. Do not synthesize placeholder connections for scheduled actions. - Sleep readiness stays centralized in
ActorContextsleep state. Queue waits, scheduled internal work, disconnect callbacks, and websocket callbacks report activity throughActorContexthooks so the idle timer stays accurate. - User-facing
onDisconnectwork runs insideActorContext::with_disconnect_callback(...)sopending_disconnect_countgates sleep until the async callback finishes.
- Registry startup builds configured
ActorContexts withActorContext::build(...)so state, queue, and connection managers inherit the actor config before lifecycle startup runs.ActorContext::build(...)must seed owned queue, connection, and sleep config storage from itsActorConfig; do not initialize those fields withActorConfig::default(). - Registry actor task handles live in one
actor_instances: SccHashMap<String, ActorInstanceState>. Useentry_asyncfor Active/Stopping transitions. RegistryDispatcher::handle_fetchowns framework HTTP routes/metadata,/health,/inspector/*,/action/*, and/queue/*. Prometheus metrics are process-wide and are never served by core itself;metrics::render_prometheus_metrics()only backs the NAPImetrics()binding andRegistry::prometheus_metrics()so hosts can mount the rendered response on their own router. TypeScript NAPI callbacks keep action/queue schema validation and queuecanPublish.- Raw
onRequestHTTP fetches bypassmaxIncomingMessageSize/maxOutgoingMessageSize. Those message-size guards apply only to/action/*and/queue/*framework routes, not unmatched useronRequestpaths. - Framework HTTP error payloads omit absent
metadatafor JSON/CBOR responses so missing metadata staysundefined. Only explicit metadatanullserializes asnull.
- Ensure the internal SQLite schema and import legacy KV to SQLite if needed.
- Load actor startup data from internal SQLite into
ActorContextbefore factory creation. - Persist
has_initializedimmediately. - Resync persisted alarms and restore hibernatable connections.
- Set
readybefore the driver hook. - Reset the sleep timer.
- Spawn
runin a detached panic-catching task. - Drain overdue scheduled events after
started. - Set
startedafter the driver hook completes.
Two-phase:
SleepGracefiresonSleepimmediately and keeps dispatch/save timers live.SleepFinalizegates dispatch, suspends alarms, and runs teardown.
Sleep grace fires the actor abort signal on entry and waits for the run handler to exit before finalize.
Finalize:
- Wait for the tracked
runtask. - Poll
ActorContextsleep state for the idle window and shutdown-task drains. - Wait for
ActorContext::wait_for_on_state_change_idle(...)before sending final save events so asynconStateChangework cannot race durability. - Persist hibernatable connections.
- Disconnect non-hibernatable connections.
- Immediate state save.
- Skip the idle-window wait.
- Use the unified
sleep_grace_periodbudget for the destroy phase. - Wait for
wait_for_on_state_change_idle(...)before final saves. - Disconnect every connection.
- Immediate state save + SQLite cleanup.
Persistence order:
- Immediate state save.
- Pending state write wait.
- Alarm write wait.
- SQLite cleanup.
- Driver alarm cancellation.
sleep_grace_period_overriddendistinguishes an explicitsleep_grace_periodfrom runtime override defaults.
- Graceful actor teardown flows through
EnvoyCallbacks::on_actor_stop_with_completion. The default implementation preserves the old immediateon_actor_stopbehavior by auto-completing the stop handle after the callback returns. - Sync
EnvoyHandlelookups for live actor state read the sharedSharedContext.actorsmirror keyed by actor id/generation. Blocking back through the envoy task can panic on current-thread Tokio runtimes.
- Boxed callback APIs use
futures::future::BoxFuture<'static, ...>plus the sharedactor::callbacks::RequestandResponsewrappers so config and HTTP parsing helpers stay in core for future runtimes.
- Process-global
ActorTasktest hooks (install_shutdown_cleanup_hook, lifecycle-event/reply hooks) must be actor-scoped and serialized in tests. Parallelcargo testruns will otherwise cross-wire unrelated actors.
- Typed
Ctx<A>stays a stateless wrapper overrivetkit-core::ActorContext. Actor state lives in the user receive loop. There is no typed vars field. CBOR encode/decode stays at wrapper method boundaries likebroadcastandConnCtx. - Typed
Ctx<A>::client()builds and cachesrivetkit-clientfrom core Envoy client accessors. Keep actor-to-actor client construction in the wrapper, not core. - Typed
Start<A>wrappers rehydrate eachActorStart.hibernatedstate blob back onto theConnHandlebefore exposingConnCtx, orconn.state()stops matching the wake snapshot. rivetkit-rust/packages/rivetkit/src/persist.rsowns typed actor-stateStateDeltabuilders.SerializeState/Sleep/Destroyinsrc/event.rsstay thin reply helpers that reuse those builders instead of open-coding persistence bytes per wrapper.