Input Parameters
Pass typed initialization input when creating Rivet Actors and use it to set up each instance when it is created.
Actors can receive input parameters when created, allowing for flexible initialization and configuration. Input is passed during actor creation and is available in lifecycle hooks.
Passing Input to Actors
Input is provided when creating actor instances using the input property:
import { actor, setup } from "rivetkit";
import { db } from "rivetkit/db";
import { createClient } from "rivetkit/client";
interface GameInput {
gameMode: string;
maxPlayers: number;
difficulty?: string;
}
const game = actor({
db: db({
onMigrate: async (db) => {
await db.execute(
"CREATE TABLE IF NOT EXISTS game (id INTEGER PRIMARY KEY CHECK (id = 1), game_mode TEXT NOT NULL, max_players INTEGER NOT NULL, difficulty TEXT NOT NULL)",
);
},
}),
onCreate: async (c, input: GameInput) => {
await c.db.execute(
"INSERT INTO game (id, game_mode, max_players, difficulty) VALUES (1, ?, ?, ?)",
input.gameMode,
input.maxPlayers,
input.difficulty ?? "medium",
);
},
actions: {}
});
const registry = setup({ use: { game } });
const client = createClient<typeof registry>("http://localhost:6420");
// Client side - create with input
const gameHandle = await client.game.create(["game-123"], {
input: {
gameMode: "tournament",
maxPlayers: 8,
difficulty: "hard",
}
});
// getOrCreate can also accept input (used only if creating)
const gameHandle2 = client.game.getOrCreate(["game-456"], {
createWithInput: {
gameMode: "casual",
maxPlayers: 4,
}
});
Accessing Input in Lifecycle Hooks
Input is available as the second argument to the onCreate lifecycle hook:
import { actor } from "rivetkit";
import { db } from "rivetkit/db";
interface ChatRoomInput {
roomName: string;
isPrivate: boolean;
maxUsers?: number;
}
// Mock function for demonstration
function setupPrivateRoomLogging(roomName: string) {
console.log(`Setting up logging for private room: ${roomName}`);
}
const chatRoom = actor({
db: db({
onMigrate: async (db) => {
await db.execute(`
CREATE TABLE IF NOT EXISTS room (
id INTEGER PRIMARY KEY CHECK (id = 1),
name TEXT NOT NULL,
is_private INTEGER NOT NULL,
max_users INTEGER NOT NULL
);
`);
await db.execute(`
CREATE TABLE IF NOT EXISTS users (
user_id TEXT PRIMARY KEY
);
`);
},
}),
onCreate: async (c, input: ChatRoomInput) => {
console.log(`Creating room: ${input.roomName}`);
// Persist the input so actions can read it later
await c.db.execute(
"INSERT INTO room (id, name, is_private, max_users) VALUES (1, ?, ?, ?)",
input?.roomName ?? "Unnamed Room",
input?.isPrivate ? 1 : 0,
input?.maxUsers ?? 50,
);
// Setup external services based on input
if (input.isPrivate) {
setupPrivateRoomLogging(input.roomName);
}
},
actions: {
// Input remains accessible in actions through SQLite
getRoomInfo: async (c) => {
const [room] = (await c.db.execute(
"SELECT name, is_private, max_users FROM room WHERE id = 1",
)) as { name: string; is_private: number; max_users: number }[];
const [users] = (await c.db.execute(
"SELECT COUNT(*) AS count FROM users",
)) as { count: number }[];
return {
name: room.name,
isPrivate: room.is_private === 1,
maxUsers: room.max_users,
currentUsers: users.count,
};
},
},
});
Input Validation
You can validate input parameters in the onCreate hook:
import { actor } from "rivetkit";
import { db } from "rivetkit/db";
import { z } from "zod";
const GameInputSchema = z.object({
gameMode: z.enum(["casual", "tournament", "ranked"]),
maxPlayers: z.number().min(2).max(16),
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
});
type GameInput = z.infer<typeof GameInputSchema>;
const game = actor({
db: db({
onMigrate: async (db) => {
await db.execute(`
CREATE TABLE IF NOT EXISTS game (
id INTEGER PRIMARY KEY CHECK (id = 1),
game_mode TEXT NOT NULL,
max_players INTEGER NOT NULL,
difficulty TEXT NOT NULL
);
`);
await db.execute(`
CREATE TABLE IF NOT EXISTS players (
player_id TEXT PRIMARY KEY
);
`);
},
}),
onCreate: async (c, inputRaw: GameInput) => {
// Validate input
const input = GameInputSchema.parse(inputRaw);
await c.db.execute(
"INSERT INTO game (id, game_mode, max_players, difficulty) VALUES (1, ?, ?, ?)",
input.gameMode,
input.maxPlayers,
input.difficulty ?? "medium",
);
},
actions: {
// Actions read the validated input back from SQLite
getGameInfo: async (c) => {
const [info] = (await c.db.execute(
"SELECT game_mode, max_players, difficulty FROM game WHERE id = 1",
)) as { game_mode: string; max_players: number; difficulty: string }[];
const [players] = (await c.db.execute(
"SELECT COUNT(*) AS count FROM players",
)) as { count: number }[];
return {
gameMode: info.game_mode,
maxPlayers: info.max_players,
difficulty: info.difficulty,
currentPlayers: players.count,
};
},
},
});
Input vs Connection Parameters
Input parameters are different from connection parameters:
- Input:
- Passed when creating the actor instance
- Use for actor-wide configuration
- Available in lifecycle hooks
- Connection parameters:
- Passed when connecting to an existing actor
- Used for connection-specific configuration
- Available in connection hooks
import { actor, setup } from "rivetkit";
import { db } from "rivetkit/db";
import { createClient } from "rivetkit/client";
interface RoomInput { roomName: string; isPrivate: boolean; }
interface ConnParams { userId: string; displayName: string; }
const chatRoom = actor({
db: db({
onMigrate: async (db) => {
await db.execute(
"CREATE TABLE IF NOT EXISTS room (id INTEGER PRIMARY KEY CHECK (id = 1), name TEXT NOT NULL, is_private INTEGER NOT NULL)",
);
},
}),
// Input configures the whole actor once, when it is created
onCreate: async (c, input: RoomInput) => {
await c.db.execute(
"INSERT INTO room (id, name, is_private) VALUES (1, ?, ?)",
input.roomName,
input.isPrivate ? 1 : 0,
);
},
// Connection parameters describe each client that connects
createVars: () => ({ users: new Map<string, ConnParams>() }),
onBeforeConnect: (c, params: ConnParams) => {
if (!params.userId) throw new Error("Missing userId");
},
onConnect: (c, conn) => {
c.vars.users.set(conn.id, conn.params);
},
onDisconnect: (c, conn) => {
c.vars.users.delete(conn.id);
},
actions: {}
});
const registry = setup({ use: { chatRoom } });
const client = createClient<typeof registry>("http://localhost:6420");
// Actor creation with input
const room = await client.chatRoom.create(["room-123"], {
input: {
roomName: "General Discussion",
isPrivate: false,
},
});
Input Best Practices
Use Type Safety
Define input types to ensure type safety:
import { actor } from "rivetkit";
import { db } from "rivetkit/db";
interface GameInput {
gameMode: "casual" | "tournament" | "ranked";
maxPlayers: number;
difficulty?: "easy" | "medium" | "hard";
}
const game = actor({
db: db({
onMigrate: async (db) => {
await db.execute(
"CREATE TABLE IF NOT EXISTS game (id INTEGER PRIMARY KEY CHECK (id = 1), game_mode TEXT NOT NULL, max_players INTEGER NOT NULL, difficulty TEXT NOT NULL)",
);
},
}),
// The input type is checked wherever the actor is created
onCreate: async (c, input: GameInput) => {
await c.db.execute(
"INSERT INTO game (id, game_mode, max_players, difficulty) VALUES (1, ?, ?, ?)",
input.gameMode,
input.maxPlayers,
input.difficulty ?? "medium",
);
},
actions: {
// Actions are now type-safe
},
});
Store Input in SQLite
Input is only available in the onCreate lifecycle hook. If you need to access input data later (in actions, timers, or other hooks), store it in SQLite during creation. This is the recommended pattern because input shapes can evolve over time, and persisting input ensures you always have access to the values the actor was created with:
import { actor } from "rivetkit";
import { db } from "rivetkit/db";
interface GameInput {
gameMode: string;
maxPlayers: number;
difficulty?: string;
}
interface GameConfig {
gameMode: string;
maxPlayers: number;
difficulty: string;
}
const game = actor({
db: db({
onMigrate: async (db) => {
await db.execute(`
CREATE TABLE IF NOT EXISTS config (
id INTEGER PRIMARY KEY CHECK (id = 1),
game_mode TEXT NOT NULL,
max_players INTEGER NOT NULL,
difficulty TEXT NOT NULL
);
`);
},
}),
// onCreate runs once, so store the input configuration in SQLite
onCreate: async (c, input: GameInput) => {
await c.db.execute(
"INSERT INTO config (id, game_mode, max_players, difficulty) VALUES (1, ?, ?, ?)",
input.gameMode,
input.maxPlayers,
input?.difficulty ?? "medium",
);
},
actions: {
getConfig: async (c): Promise<GameConfig> => {
const [row] = (await c.db.execute(
"SELECT game_mode, max_players, difficulty FROM config WHERE id = 1",
)) as { game_mode: string; max_players: number; difficulty: string }[];
return {
gameMode: row.game_mode,
maxPlayers: row.max_players,
difficulty: row.difficulty,
};
},
updateDifficulty: async (c, difficulty: string) => {
await c.db.execute("UPDATE config SET difficulty = ? WHERE id = 1", difficulty);
},
},
});