Actors follow a well-defined lifecycle with hooks at each stage. Understanding these hooks is essential for proper initialization, state management, and cleanup.
Actors can receive input parameters when created, allowing for flexible initialization:
Use Zod to define a schema for input validation:
import { actor } from "@rivetkit/actor";
import { z } from "zod";
const gameActor = actor({
createState: (c, opts) => ({
mode: opts.input?.gameMode ?? "classic",
maxPlayers: opts.input?.maxPlayers ?? 4,
difficulty: opts.input?.difficulty ?? "medium",
players: [],
status: "waiting",
}),
actions: {
// Actions can access input via context
getGameConfig: (c) => ({
mode: c.state.mode,
maxPlayers: c.state.maxPlayers,
difficulty: c.state.difficulty,
}),
},
});
Input is provided when creating actor instances:
// Client side - create with input
const game = await client.game.create(["game-123"], {
gameMode: "tournament",
maxPlayers: 8,
difficulty: "hard",
});
// getOrCreate can also accept input (used only if creating)
const gameHandle = client.game.getOrCreate(["game-456"], {
gameMode: "casual",
maxPlayers: 4,
});
Input is available in lifecycle hooks via the opts
parameter:
const chatRoom = actor({
createState: (c, opts) => ({
name: opts.input?.roomName ?? "Unnamed Room",
isPrivate: opts.input?.isPrivate ?? false,
maxUsers: opts.input?.maxUsers ?? 50,
users: {},
messages: [],
}),
onCreate: (c, opts) => {
console.log(`Creating room: ${opts.input?.roomName}`);
// Setup external services based on input
if (opts.input?.isPrivate) {
setupPrivateRoomLogging(opts.input.roomName);
}
},
actions: {
// Input remains accessible in actions
getRoomInfo: (c) => ({
name: c.state.name,
isPrivate: c.state.isPrivate,
maxUsers: c.state.maxUsers,
currentUsers: Object.keys(c.state.users).length,
}),
},
});
Lifecycle Hooks
Actor lifecycle hooks are defined as functions in the actor configuration.
createState
and state
The createState
function or state
constant defines the initial state of the actor (see state documentation). The createState
function is called only once when the actor is first created.
createVars
and vars
The createVars
function or vars
constant defines ephemeral variables for the actor (see state documentation). These variables are not persisted and are useful for storing runtime-only objects or temporary data.
The createVars
function can also receive driver-specific context as its second parameter, allowing access to driver capabilities like Rivet KV or Cloudflare Durable Object storage.
import { actor } from "@rivetkit/actor";
// Using vars constant
const counter1 = actor({
state: { count: 0 },
vars: { lastAccessTime: 0 },
actions: { /* ... */ }
});
// Using createVars function
const counter2 = actor({
state: { count: 0 },
createVars: () => {
// Initialize with non-serializable objects
return {
lastAccessTime: Date.now(),
emitter: new EventTarget()
};
},
actions: { /* ... */ }
});
// Access driver-specific context
const exampleActor = actor({
state: { count: 0 },
// Access driver context in createVars
createVars: (c, driverCtx) => ({
driverCtx,
}),
actions: {
doSomething: (c) => {
// Use driver-specific context
console.log("Driver context:", c.vars.driverCtx);
}
}
});
onCreate
The onCreate
hook is called at the same time as createState
, but unlike createState
, it doesn’t return any value. Use this hook for initialization logic that doesn’t affect the initial state.
import { actor } from "@rivetkit/actor";
// Using state constant
const counter1 = actor({
state: { count: 0 },
actions: { /* ... */ }
});
// Using createState function
const counter2 = actor({
createState: () => {
// Initialize with a count of 0
return { count: 0 };
},
actions: { /* ... */ }
});
// Using onCreate
const counter3 = actor({
state: { count: 0 },
// Run initialization logic (logging, external service setup, etc.)
onCreate: (c, opts) => {
console.log("Counter actor initialized");
// Access input parameters if provided
console.log("Input:", opts.input);
// Can perform async operations or setup
// No need to return anything
},
actions: { /* ... */ }
});
onStart
This hook is called any time the actor is started (e.g. after restarting, upgrading code, or crashing).
This is called after the actor has been initialized but before any connections are accepted.
Use this hook to set up any resources or start any background tasks, such as setInterval
.
import { actor } from "@rivetkit/actor";
const counter = actor({
state: { count: 0 },
vars: { intervalId: null as NodeJS.Timeout | null },
onStart: (c) => {
console.log('Actor started with count:', c.state.count);
// Set up interval for automatic counting
const intervalId = setInterval(() => {
c.state.count++;
c.broadcast("countChanged", c.state.count);
console.log('Auto-increment:', c.state.count);
}, 10000);
// Store interval ID in vars to clean up later if needed
c.vars.intervalId = intervalId;
},
actions: {
stop: (c) => {
if (c.vars.intervalId) {
clearInterval(c.vars.intervalId);
c.vars.intervalId = null;
}
}
}
});
onStateChange
Called whenever the actor’s state changes. This is often used to broadcast state updates.
import { actor } from "@rivetkit/actor";
const counter = actor({
state: { count: 0 },
onStateChange: (c, newState) => {
// Broadcast the new count to all connected clients
c.broadcast('countUpdated', {
count: newState.count
});
},
actions: {
increment: (c) => {
c.state.count++;
return c.state.count;
}
}
});
createConnState
and connState
There are two ways to define the initial state for connections:
connState
: Define a constant object that will be used as the initial state for all connections
createConnState
: A function that dynamically creates initial connection state based on connection parameters
onBeforeConnect
The onBeforeConnect
hook is called whenever a new client connects to the actor. 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.
import { actor } from "@rivetkit/actor";
const chatRoom = actor({
state: { messages: [] },
// Method 1: Use a static default connection state
connState: {
role: "guest",
joinTime: 0,
},
// Method 2: Dynamically create connection state
createConnState: (c, { params }) => {
return {
userId: params.userId || "anonymous",
role: params.role || "guest",
joinTime: Date.now()
};
},
// Validate connections before accepting them
onBeforeConnect: (c, { params }) => {
// Validate authentication
const authToken = params.authToken;
if (!authToken || !validateToken(authToken)) {
throw new Error("Invalid authentication");
}
// Authentication is valid, connection will proceed
// The actual connection state will come from connState or createConnState
},
actions: { /* ... */ }
});
Connections cannot interact with the actor until this method completes successfully. Throwing an error will abort the connection. This can be used for authentication - see Authentication for details.
onConnect
Executed after the client has successfully connected.
import { actor } from "@rivetkit/actor";
const chatRoom = actor({
state: { users: {}, messages: [] },
onConnect: (c) => {
// Add user to the room's user list using connection state
const userId = c.conn.state.userId;
c.state.users[userId] = {
online: true,
lastSeen: Date.now()
};
// Broadcast that a user joined
c.broadcast("userJoined", { userId, timestamp: Date.now() });
console.log(`User ${userId} connected`);
},
actions: { /* ... */ }
});
Messages will not be processed for this actor until this hook succeeds. Errors thrown from this hook will cause the client to disconnect.
onDisconnect
Called when a client disconnects from the actor. Use this to clean up any connection-specific resources.
import { actor } from "@rivetkit/actor";
const chatRoom = actor({
state: { users: {}, messages: [] },
onDisconnect: (c) => {
// Update user status when they disconnect
const userId = c.conn.state.userId;
if (c.state.users[userId]) {
c.state.users[userId].online = false;
c.state.users[userId].lastSeen = Date.now();
}
// Broadcast that a user left
c.broadcast("userLeft", { userId, timestamp: Date.now() });
console.log(`User ${userId} disconnected`);
},
actions: { /* ... */ }
});
Destroying Actors
Actors can be shut down gracefully with c.shutdown()
. Clients will be gracefully disconnected.
import { actor } from "@rivetkit/actor";
const temporaryRoom = actor({
state: {
createdAt: 0,
expiresAfterMs: 3600000 // 1 hour
},
createState: () => ({
createdAt: Date.now(),
expiresAfterMs: 3600000 // 1 hour
}),
onStart: (c) => {
// Check if room is expired
const now = Date.now();
const expiresAt = c.state.createdAt + c.state.expiresAfterMs;
if (now > expiresAt) {
console.log("Room expired, shutting down");
c.shutdown();
} else {
// Set up expiration timer
const timeUntilExpiry = expiresAt - now;
setTimeout(() => {
console.log("Room lifetime reached, shutting down");
c.shutdown();
}, timeUntilExpiry);
}
},
actions: {
closeRoom: (c) => {
// Notify all clients
c.broadcast("roomClosed", { reason: "Admin closed the room" });
// Shutdown the actor
c.shutdown();
}
}
});
This action is permanent and cannot be reverted.
Using ActorContext
Type Externally
When extracting logic from lifecycle hooks or actions into external functions, you’ll often need to define the type of the context parameter. RivetKit provides helper types that make it easy to extract and pass these context types to external functions.
import { actor, ActorContextOf } from "@rivetkit/actor";
const myActor = actor({
state: { count: 0 },
// Use external function in lifecycle hook
onStart: (c) => logActorStarted(c)
});
// Simple external function with typed context
function logActorStarted(c: ActorContextOf<typeof myActor>) {
console.log(`Actor started with count: ${c.state.count}`);
}
See Helper Types for more details on using ActorContextOf
.
Full Example
import { actor } from "@rivetkit/actor";
import { z } from "zod";
const counter = actor({
// Initialize state with input
createState: (c, opts) => ({
count: opts.input?.initialCount ?? 0,
stepSize: opts.input?.stepSize ?? 1,
name: opts.input?.name ?? "Unnamed Counter",
}),
// Initialize actor (run setup that doesn't affect initial state)
onCreate: (c, opts) => {
console.log(`Counter "${opts.input?.name}" initialized`);
// Set up external resources, logging, etc.
},
// Define default connection state
connState: {
role: "guest"
},
// Dynamically create connection state based on params
createConnState: (c, { params }) => {
// Get auth info from validation
const authToken = params.authToken;
const authInfo = validateAuthToken(authToken);
return {
userId: authInfo?.userId || "anonymous",
role: authInfo?.role || "guest"
};
},
// Lifecycle hooks
onStart: (c) => {
console.log(`Counter "${c.state.name}" started with count:`, c.state.count);
},
onStateChange: (c, newState) => {
c.broadcast('countUpdated', {
count: newState.count,
name: newState.name
});
},
onBeforeConnect: (c, { params }) => {
// Validate auth token
const authToken = params.authToken;
if (!authToken) {
throw new Error('Missing auth token');
}
// Validate with your API and determine the user
const authInfo = validateAuthToken(authToken);
if (!authInfo) {
throw new Error('Invalid auth token');
}
// If validation succeeds, connection proceeds
// Connection state will be set by createConnState
},
onConnect: (c) => {
console.log(`User ${c.conn.state.userId} connected to "${c.state.name}"`);
},
onDisconnect: (c) => {
console.log(`User ${c.conn.state.userId} disconnected from "${c.state.name}"`);
},
// Define actions
actions: {
increment: (c, amount?: number) => {
const step = amount ?? c.state.stepSize;
c.state.count += step;
return c.state.count;
},
reset: (c) => {
// Check if user has admin role
if (c.conn.state.role !== "admin") {
throw new Error("Unauthorized: requires admin role");
}
c.state.count = 0;
return c.state.count;
},
getInfo: (c) => ({
name: c.state.name,
count: c.state.count,
stepSize: c.state.stepSize,
}),
}
});
export default counter;