Sign In
Lifecycle & Config

Lifecycle

Understand actor lifecycle hooks and initialization patterns

Actors follow a well-defined lifecycle with hooks at each stage. Understanding these hooks is essential for proper initialization, state management, and cleanup.

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.

TypeScript
import { actor, ActorInitContext } from "@rivetkit/actor";
// In this example, assume we're using Redis
import type { DriverContext } from "@rivetkit/redis";

// 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: ActorInitContext, driverCtx: DriverContext) => ({
    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.

TypeScript
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, input: { foo: string }) => {
    console.log("Counter actor initialized");
    // Access input parameters if provided
    console.log("Input:", 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.

TypeScript
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.

TypeScript
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:

  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

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.

TypeScript
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, opts, params: { userId?: string, role?: string }) => {
    return {
      userId: params.userId || "anonymous",
      role: params.role || "guest",
      joinTime: Date.now()
    };
  },
  
  // Validate connections before accepting them
  onBeforeConnect: (c, opts, params: { authToken?: string }) => {
    // 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.

TypeScript
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.

TypeScript
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: { /* ... */ }
});

onFetch

The onFetch hook handles HTTP requests sent to your actor. It receives the actor context and a standard Request object, and should return a Response object or void to continue default routing.

TypeScript
import { actor } from "@rivetkit/actor";

const apiActor = actor({
  state: { requestCount: 0 },
  
  onFetch: (c, request) => {
    const url = new URL(request.url);
    c.state.requestCount++;
    
    if (url.pathname === "/api/status") {
      return new Response(JSON.stringify({
        status: "ok",
        requestCount: c.state.requestCount
      }), {
        headers: { "Content-Type": "application/json" }
      });
    }
    
    // Return void to continue to default routing
    return;
  },
  
  actions: { /* ... */ }
});

onWebSocket

The onWebSocket hook handles WebSocket connections to your actor. It receives the actor context, a WebSocket object, and the initial Request. Use this to set up WebSocket event listeners and handle real-time communication.

TypeScript
import { actor } from "@rivetkit/actor";

const realtimeActor = actor({
  state: { connectionCount: 0 },
  
  onWebSocket: (c, websocket, request) => {
    c.state.connectionCount++;
    
    // Send welcome message
    websocket.send(JSON.stringify({
      type: "welcome",
      connectionCount: c.state.connectionCount
    }));
    
    // Handle incoming messages
    websocket.addEventListener("message", (event) => {
      const data = JSON.parse(event.data);
      
      if (data.type === "ping") {
        websocket.send(JSON.stringify({
          type: "pong",
          timestamp: Date.now()
        }));
      }
    });
    
    // Handle connection close
    websocket.addEventListener("close", () => {
      c.state.connectionCount--;
    });
  },
  
  actions: { /* ... */ }
});

onAuth

The onAuth hook is called on the HTTP server before clients can interact with the actor. This hook is required for any public HTTP endpoint access and is used to validate client credentials and return authentication data that will be available on connections.

This hook runs on the HTTP server (not the actor) to reduce load and prevent denial of service attacks against individual actors. Only called for public endpoints - calls to actors from within the backend do not trigger this handler.

TypeScript
import { actor } from "@rivetkit/actor";

const secureActor = actor({
  // Authentication handler - runs on HTTP server
  onAuth: async (params: undefined, { request }) => {
    const authHeader = request.headers.get("authorization");
    if (!authHeader?.startsWith("Bearer ")) {
      throw new Error("Missing or invalid authorization header");
    }
    
    const token = authHeader.slice(7);
    
    // Validate token with your auth service
    const user = await validateAuthToken(token);
    if (!user) {
      throw new Error("Invalid authentication token");
    }
    
    // Return auth data (must be serializable)
    return {
      userId: user.id,
      role: user.role,
      permissions: user.permissions
    };
  },
  
  state: { activeUsers: {} },
  
  onConnect: (c) => {
    // Access auth data from onAuth
    const userId = c.conn.auth.userId;
    const role = c.conn.auth.role;
    
    console.log(`User ${userId} with role ${role} connected`);
    
    c.state.activeUsers[userId] = {
      role,
      connectedAt: Date.now()
    };
  },
  
  actions: {
    adminAction: (c) => {
      // Check permissions from auth data
      if (c.conn.auth.role !== "admin") {
        throw new Error("Unauthorized: admin role required");
      }
      
      return { message: "Admin action completed" };
    }
  }
});

onBeforeActionResponse

The onBeforeActionResponse hook is called before sending an action response to the client. 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.

TypeScript
import { actor } from "@rivetkit/actor";

const loggingActor = actor({
  state: { requestCount: 0 },
  
  onBeforeActionResponse: (c, actionName, args, output) => {
    // Log action calls
    console.log(`Action ${actionName} called with args:`, args);
    console.log(`Action ${actionName} returned:`, output);
    
    // Add metadata to all responses
    return {
      data: output,
      metadata: {
        actionName,
        timestamp: Date.now(),
        requestId: crypto.randomUUID(),
        userId: c.conn.auth?.userId
      }
    };
  },
  
  actions: {
    getUserData: (c, userId: string) => {
      c.state.requestCount++;
      
      // This will be wrapped with metadata by onBeforeActionResponse
      return {
        userId,
        profile: { name: "John Doe", email: "[email protected]" },
        lastActive: Date.now()
      };
    },
    
    getStats: (c) => {
      // This will also be wrapped with metadata
      return {
        requestCount: c.state.requestCount,
        uptime: process.uptime()
      };
    }
  }
});

Destroying Actors

Destroying actors is not available yet.

Advanced

Running Background Tasks

The c.runInBackground method allows you to execute promises asynchronously without blocking the actor's main execution flow. The actor is prevented from sleeping while the promise passed to runInBackground is still active. This is useful for fire-and-forget operations where you don't need to wait for completion.

Common use cases:

  • Analytics and logging: Send events to external services without delaying responses
  • State sync: Populate external databases or APIs with updates to actor state in the background
TypeScript
import { actor } from "@rivetkit/actor";

const gameRoom = actor({
  state: { players: {}, scores: {} },
  
  actions: {
    playerJoined: (c, playerId: string) => {
      c.state.players[playerId] = { joinedAt: Date.now() };
      
      // Send analytics event without blocking
      c.runInBackground(
        fetch('https://analytics.example.com/events', {
          method: 'POST',
          body: JSON.stringify({ 
            event: 'player_joined', 
            playerId,
            timestamp: Date.now() 
          })
        }).then(() => console.log('Analytics sent'))
      );
      
      return { success: true };
    },
  }
});

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. Rivet provides helper types that make it easy to extract and pass these context types to external functions.

TypeScript
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

TypeScript
import { actor, ActorInitContext } from "@rivetkit/actor";
import { z } from "zod";

interface CounterInput {
	initialCount?: number;
	stepSize?: number;
	name?: string;
}

const counter = actor({
  // Authentication handler (runs on HTTP server)
  onAuth: async (opts) => {
    const authHeader = opts.req.headers.get("authorization");
    if (!authHeader?.startsWith("Bearer ")) {
      throw new Error("Missing authorization header");
    }
    
    const token = authHeader.slice(7);
    const user = await validateAuthToken(token);
    if (!user) {
      throw new Error("Invalid auth token");
    }
    
    return {
      userId: user.id,
      role: user.role
    };
  },
  
  // Initialize state with input
  createState: (c: ActorInitContext, input: CounterInput) => ({
    count: input.initialCount ?? 0,
    stepSize: input.stepSize ?? 1,
    name: input.name ?? "Unnamed Counter",
    requestCount: 0,
  }),
  
  // Initialize actor (run setup that doesn't affect initial state)
  onCreate: (c, opts, input: { name: string }) => {
    console.log(`Counter "${input.name}" initialized`);
    // Set up external resources, logging, etc.
  },
  
  // Define default connection state
  connState: {
    role: "guest"
  },
  
  createConnState: (c, opts) => {
    // Auth data is available from onAuth
    return {
      userId: c.conn.auth.userId,
      role: c.conn.auth.role
    };
  },
  
  // 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 }) => {
    // Additional validation can be done here if needed
    // Auth was already validated in onAuth
    console.log(`User ${c.conn.auth.userId} attempting to connect`);
  },
  
  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}"`);
  },
  
  // Transform all action responses
  onBeforeActionResponse: (c, actionName, args, output) => {
    c.state.requestCount++;
    
    return {
      data: output,
      metadata: {
        action: actionName,
        timestamp: Date.now(),
        userId: c.conn.auth.userId,
        requestNumber: c.state.requestCount
      }
    };
  },
  
  // 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.auth.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,
      totalRequests: c.state.requestCount,
    }),
  }
});

export default counter;
Suggest changes to this page