diff --git a/examples/kitchen-sink/README.md b/examples/kitchen-sink/README.md
new file mode 100644
index 000000000..66221e34c
--- /dev/null
+++ b/examples/kitchen-sink/README.md
@@ -0,0 +1,36 @@
+# Kitchen Sink Example for RivetKit
+
+Example project demonstrating all RivetKit features with [RivetKit](https://rivetkit.org).
+
+[Learn More →](https://github.com/rivet-dev/rivetkit)
+
+[Discord](https://rivet.dev/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-dev/rivetkit/issues)
+
+## Getting Started
+
+### Prerequisites
+
+- Node.js 18+ or Bun
+- RivetKit development environment
+
+### Installation
+
+```sh
+git clone https://github.com/rivet-dev/rivetkit
+cd rivetkit/examples/kitchen-sink
+npm install
+```
+
+### Development
+
+```sh
+npm run dev
+```
+
+This will start both the backend server (port 8080) and frontend development server (port 5173).
+
+Open [http://localhost:5173](http://localhost:5173) in your browser.
+
+## License
+
+Apache 2.0
diff --git a/examples/kitchen-sink/SPEC.md b/examples/kitchen-sink/SPEC.md
new file mode 100644
index 000000000..ebbc7f3b1
--- /dev/null
+++ b/examples/kitchen-sink/SPEC.md
@@ -0,0 +1,126 @@
+# Kitchen Sink Example Specification
+
+## Overview
+Comprehensive example demonstrating all RivetKit features with a simple React frontend.
+
+## UI Structure
+
+### Header (Always visible)
+
+**Configuration Row 1:**
+- **Transport**: WebSocket ↔ SSE toggle
+- **Encoding**: JSON ↔ CBOR ↔ Bare dropdown
+- **Connection Mode**: Handle ↔ Connection toggle
+
+**Actor Management Row 2:**
+- **Actor Name**: dropdown
+- **Key**: input
+- **Region**: input
+- **Input JSON**: textarea
+- **Buttons**: `create()` | `get()` | `getOrCreate()` | `getForId()` (when no actor connected)
+- **OR Button**: `dispose()` (when actor connected)
+
+**Status Row 3:**
+- Connection status indicator with actor info (Name | Key | Actor ID | Status)
+
+### Main Content (8 Tabs - disabled until actor connected)
+
+#### 1. Actions
+- Action name dropdown/input
+- Arguments JSON textarea
+- Call button
+- Response display
+
+#### 2. Events
+- Event listener controls
+- Live event feed with timestamps
+- Event type filter
+
+#### 3. Schedule
+- `schedule.at()` with timestamp input
+- `schedule.after()` with delay input
+- Scheduled task history
+- Custom alarm data input
+
+#### 4. Sleep
+- `sleep()` button
+- Sleep timeout configuration
+- Lifecycle event display (`onStart`, `onStop`)
+
+#### 5. Connections (Only visible in Connection Mode)
+- Connection state display
+- Connection info (ID, transport, encoding)
+- Connection-specific event history
+
+#### 6. Raw HTTP
+- HTTP method dropdown
+- Path input
+- Headers textarea
+- Body textarea
+- Send button & response display
+
+#### 7. Raw WebSocket
+- Message input (text/binary toggle)
+- Send button
+- Message history (sent/received with timestamps)
+
+#### 8. Metadata
+- Actor name, tags, region display
+
+## User Flow
+1. Configure transport/encoding/connection mode
+2. Fill actor details (name, key, region, input)
+3. Click create/get/getOrCreate/getForId
+4. Status shows connection info, tabs become enabled
+5. Use tabs to test features
+6. Click dispose() to disconnect and return to step 1
+
+## Actors
+
+### 1. `demo` - Main comprehensive actor
+- All action types (sync/async/promise)
+- Events and state management
+- Scheduling capabilities (`schedule.at()`, `schedule.after()`)
+- Sleep functionality with configurable timeout
+- Connection state support
+- Lifecycle hooks (`onStart`, `onStop`, `onConnect`, `onDisconnect`)
+- Metadata access
+
+### 2. `http` - Raw HTTP handling
+- `onFetch()` handler
+- Multiple endpoint examples
+- Various HTTP methods support
+
+### 3. `websocket` - Raw WebSocket handling
+- `onWebSocket()` handler
+- Text and binary message support
+- Connection lifecycle management
+
+## Features Demonstrated
+
+### Core Features
+- **Actions**: sync, async, promise-based with various input types
+- **Events**: broadcasting to connected clients
+- **State Management**: actor state + per-connection state
+- **Scheduling**: `schedule.at()`, `schedule.after()` with alarm handlers
+- **Force Sleep**: `sleep()` method with configurable sleep timeout
+- **Lifecycle Hooks**: `onStart`, `onStop`, `onConnect`, `onDisconnect`
+
+### Configuration Options
+- **Transport**: WebSocket vs Server-Sent Events
+- **Encoding**: JSON, CBOR, Bare
+
+### Raw Protocols
+- **Raw HTTP**: Direct HTTP request handling
+- **Raw WebSocket**: Direct WebSocket connection handling
+
+### Connection Patterns
+- **Handle Mode**: Fire-and-forget action calls
+- **Connection Mode**: Persistent connection with real-time events
+
+### Actor Management
+- **Create**: `client.actor.create(key, opts)`
+- **Get**: `client.actor.get(key, opts)`
+- **Get or Create**: `client.actor.getOrCreate(key, opts)`
+- **Get by ID**: `client.actor.getForId(actorId, opts)`
+- **Dispose**: `client.dispose()` - disconnect all connections
\ No newline at end of file
diff --git a/examples/kitchen-sink/index.html b/examples/kitchen-sink/index.html
new file mode 100644
index 000000000..04d64ac54
--- /dev/null
+++ b/examples/kitchen-sink/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ RivetKit Kitchen Sink
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/kitchen-sink/package.json b/examples/kitchen-sink/package.json
new file mode 100644
index 000000000..3413bacf4
--- /dev/null
+++ b/examples/kitchen-sink/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "example-kitchen-sink",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"",
+ "dev:backend": "tsx --watch src/backend/server.ts",
+ "dev:frontend": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "check-types": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@rivetkit/react": "workspace:*",
+ "rivetkit": "workspace:*",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.66",
+ "@types/react-dom": "^18.2.22",
+ "@vitejs/plugin-react": "^4.2.1",
+ "concurrently": "^8.2.2",
+ "tsx": "^4.7.1",
+ "typescript": "^5.2.2",
+ "vite": "^5.2.0"
+ }
+}
diff --git a/examples/kitchen-sink/src/backend/actors/demo.ts b/examples/kitchen-sink/src/backend/actors/demo.ts
new file mode 100644
index 000000000..0c7252d5d
--- /dev/null
+++ b/examples/kitchen-sink/src/backend/actors/demo.ts
@@ -0,0 +1,155 @@
+import { actor } from "rivetkit";
+import { handleHttpRequest, httpActions } from "./http";
+import { handleWebSocket, websocketActions } from "./websocket";
+
+export const demo = actor({
+ createState: (_c, input) => ({
+ input,
+ count: 0,
+ lastMessage: "",
+ alarmHistory: [] as { id: string; time: number; data?: any }[],
+ startCount: 0,
+ stopCount: 0,
+ }),
+ connState: {
+ connectionTime: 0,
+ },
+ onStart: (c) => {
+ c.state.startCount += 1;
+ c.log.info({ msg: "demo actor started", startCount: c.state.startCount });
+ },
+ onStop: (c) => {
+ c.state.stopCount += 1;
+ c.log.info({ msg: "demo actor stopped", stopCount: c.state.stopCount });
+ },
+ onConnect: (c, conn) => {
+ conn.state.connectionTime = Date.now();
+ c.log.info({
+ msg: "client connected",
+ connectionTime: conn.state.connectionTime,
+ });
+ },
+ onDisconnect: (c) => {
+ c.log.info("client disconnected");
+ },
+ onFetch: handleHttpRequest,
+ onWebSocket: handleWebSocket,
+ actions: {
+ // Sync actions
+ increment: (c, amount: number = 1) => {
+ c.state.count += amount;
+ c.broadcast("countChanged", { count: c.state.count, amount });
+ return c.state.count;
+ },
+ getCount: (c) => {
+ return c.state.count;
+ },
+ setMessage: (c, message: string) => {
+ c.state.lastMessage = message;
+ c.broadcast("messageChanged", { message });
+ return message;
+ },
+
+ // Async actions
+ delayedIncrement: async (c, amount: number = 1, delayMs: number = 1000) => {
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
+ c.state.count += amount;
+ c.broadcast("countChanged", { count: c.state.count, amount });
+ return c.state.count;
+ },
+
+ // Promise action
+ promiseAction: () => {
+ return Promise.resolve({
+ timestamp: Date.now(),
+ message: "promise resolved",
+ });
+ },
+
+ // State management
+ getState: (c) => {
+ return {
+ actorState: c.state,
+ connectionState: c.conn.state,
+ };
+ },
+
+ // Scheduling
+ scheduleAlarmAt: (c, timestamp: number, data?: any) => {
+ const id = `alarm-${Date.now()}`;
+ c.schedule.at(timestamp, "onAlarm", { id, data });
+ return { id, scheduledFor: timestamp };
+ },
+ scheduleAlarmAfter: (c, delayMs: number, data?: any) => {
+ const id = `alarm-${Date.now()}`;
+ c.schedule.after(delayMs, "onAlarm", { id, data });
+ return { id, scheduledFor: Date.now() + delayMs };
+ },
+ onAlarm: (c, payload: { id: string; data?: any }) => {
+ const alarmEntry = { ...payload, time: Date.now() };
+ c.state.alarmHistory.push(alarmEntry);
+ c.broadcast("alarmTriggered", alarmEntry);
+ c.log.info({ msg: "alarm triggered", ...alarmEntry });
+ },
+ getAlarmHistory: (c) => {
+ return c.state.alarmHistory;
+ },
+ clearAlarmHistory: (c) => {
+ c.state.alarmHistory = [];
+ return true;
+ },
+
+ // Sleep
+ triggerSleep: (c) => {
+ c.sleep();
+ return "sleep triggered";
+ },
+
+ // Lifecycle info
+ getLifecycleInfo: (c) => {
+ return {
+ startCount: c.state.startCount,
+ stopCount: c.state.stopCount,
+ };
+ },
+
+ // Metadata
+ getMetadata: (c) => {
+ return {
+ name: c.name,
+ };
+ },
+ getInput: (c) => {
+ return c.state.input;
+ },
+ getActorState: (c) => {
+ return c.state;
+ },
+ getConnState: (c) => {
+ return c.conn.state;
+ },
+
+ // Events
+ broadcastCustomEvent: (c, eventName: string, data: any) => {
+ c.broadcast(eventName, data);
+ return { eventName, data, timestamp: Date.now() };
+ },
+
+ // Connections
+ listConnections: (c) => {
+ return Array.from(c.conns.values()).map((conn) => ({
+ id: conn.id,
+ connectedAt: conn.state.connectionTime,
+ }));
+ },
+
+ // HTTP actions
+ ...httpActions,
+
+ // WebSocket actions
+ ...websocketActions,
+ },
+ options: {
+ sleepTimeout: 2000,
+ },
+});
diff --git a/examples/kitchen-sink/src/backend/actors/http.ts b/examples/kitchen-sink/src/backend/actors/http.ts
new file mode 100644
index 000000000..ff96bf097
--- /dev/null
+++ b/examples/kitchen-sink/src/backend/actors/http.ts
@@ -0,0 +1,148 @@
+import type { ActorContext } from "rivetkit";
+
+export function handleHttpRequest(
+ c: ActorContext,
+ request: Request,
+) {
+ const url = new URL(request.url);
+ const method = request.method;
+ const path = url.pathname;
+
+ // Track request
+ if (!c.state.requestCount) c.state.requestCount = 0;
+ if (!c.state.requestHistory) c.state.requestHistory = [];
+
+ c.state.requestCount++;
+ c.state.requestHistory.push({
+ method,
+ path,
+ timestamp: Date.now(),
+ headers: Object.fromEntries(request.headers.entries()),
+ });
+
+ c.log.info({
+ msg: "http request received",
+ method,
+ path,
+ fullUrl: request.url,
+ requestCount: c.state.requestCount,
+ });
+
+ // Handle different endpoints
+ if (path === "/api/hello") {
+ return new Response(
+ JSON.stringify({
+ message: "Hello from HTTP actor!",
+ timestamp: Date.now(),
+ requestCount: c.state.requestCount,
+ }),
+ {
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+ }
+
+ if (path === "/api/echo" && method === "POST") {
+ return new Response(request.body, {
+ headers: {
+ "Content-Type": request.headers.get("Content-Type") || "text/plain",
+ "X-Echo-Timestamp": Date.now().toString(),
+ },
+ });
+ }
+
+ if (path === "/api/stats") {
+ return new Response(
+ JSON.stringify({
+ requestCount: c.state.requestCount,
+ requestHistory: c.state.requestHistory.slice(-10), // Last 10 requests
+ }),
+ {
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+ }
+
+ if (path === "/api/headers") {
+ const headers = Object.fromEntries(request.headers.entries());
+ return new Response(
+ JSON.stringify({
+ headers,
+ method,
+ path,
+ timestamp: Date.now(),
+ }),
+ {
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+ }
+
+ if (path === "/api/json" && method === "POST") {
+ return request.json().then((body) => {
+ return new Response(
+ JSON.stringify({
+ received: body,
+ method,
+ timestamp: Date.now(),
+ processed: true,
+ }),
+ {
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+ });
+ }
+
+ // Handle custom paths with query parameters
+ if (path.startsWith("/api/custom")) {
+ const searchParams = Object.fromEntries(url.searchParams.entries());
+ return new Response(
+ JSON.stringify({
+ path,
+ method,
+ queryParams: searchParams,
+ timestamp: Date.now(),
+ message: "Custom endpoint response",
+ }),
+ {
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+ }
+
+ // Return 404 for unhandled paths
+ return new Response(
+ JSON.stringify({
+ error: "Not Found",
+ path,
+ method,
+ availableEndpoints: [
+ "/api/hello",
+ "/api/echo",
+ "/api/stats",
+ "/api/headers",
+ "/api/json",
+ "/api/custom/*",
+ ],
+ }),
+ {
+ status: 404,
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+}
+
+export const httpActions = {
+ getHttpStats: (c: any) => {
+ return {
+ requestCount: c.state.requestCount || 0,
+ requestHistory: c.state.requestHistory || [],
+ };
+ },
+ clearHttpHistory: (c: any) => {
+ c.state.requestHistory = [];
+ c.state.requestCount = 0;
+ return true;
+ },
+};
diff --git a/examples/kitchen-sink/src/backend/actors/websocket.ts b/examples/kitchen-sink/src/backend/actors/websocket.ts
new file mode 100644
index 000000000..ebb0d69b7
--- /dev/null
+++ b/examples/kitchen-sink/src/backend/actors/websocket.ts
@@ -0,0 +1,194 @@
+import type { UniversalWebSocket } from "rivetkit";
+
+export function handleWebSocket(
+ c: any,
+ websocket: UniversalWebSocket,
+ opts: any,
+) {
+ const connectionId = `conn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+
+ // Initialize WebSocket state if not exists
+ if (!c.state.connectionCount) c.state.connectionCount = 0;
+ if (!c.state.messageCount) c.state.messageCount = 0;
+ if (!c.state.messageHistory) c.state.messageHistory = [];
+
+ c.state.connectionCount++;
+ c.log.info("websocket connected", {
+ connectionCount: c.state.connectionCount,
+ connectionId,
+ url: opts.request.url,
+ });
+
+ // Send welcome message
+ const welcomeMessage = JSON.stringify({
+ type: "welcome",
+ connectionId,
+ connectionCount: c.state.connectionCount,
+ timestamp: Date.now(),
+ });
+
+ websocket.send(welcomeMessage);
+ c.state.messageHistory.push({
+ type: "sent",
+ data: welcomeMessage,
+ timestamp: Date.now(),
+ connectionId,
+ });
+
+ // Handle incoming messages
+ websocket.addEventListener("message", (event: any) => {
+ c.state.messageCount++;
+ const timestamp = Date.now();
+
+ c.log.info("websocket message received", {
+ messageCount: c.state.messageCount,
+ connectionId,
+ dataType: typeof event.data,
+ });
+
+ // Record received message
+ c.state.messageHistory.push({
+ type: "received",
+ data: event.data,
+ timestamp,
+ connectionId,
+ });
+
+ const data = event.data;
+
+ if (typeof data === "string") {
+ try {
+ const parsed = JSON.parse(data);
+
+ if (parsed.type === "ping") {
+ const pongMessage = JSON.stringify({
+ type: "pong",
+ timestamp,
+ originalTimestamp: parsed.timestamp,
+ });
+ websocket.send(pongMessage);
+ c.state.messageHistory.push({
+ type: "sent",
+ data: pongMessage,
+ timestamp,
+ connectionId,
+ });
+ } else if (parsed.type === "echo") {
+ const echoMessage = JSON.stringify({
+ type: "echo-response",
+ originalMessage: parsed.message,
+ timestamp,
+ });
+ websocket.send(echoMessage);
+ c.state.messageHistory.push({
+ type: "sent",
+ data: echoMessage,
+ timestamp,
+ connectionId,
+ });
+ } else if (parsed.type === "getStats") {
+ const statsMessage = JSON.stringify({
+ type: "stats",
+ connectionCount: c.state.connectionCount,
+ messageCount: c.state.messageCount,
+ timestamp,
+ });
+ websocket.send(statsMessage);
+ c.state.messageHistory.push({
+ type: "sent",
+ data: statsMessage,
+ timestamp,
+ connectionId,
+ });
+ } else if (parsed.type === "broadcast") {
+ // Broadcast to all connections would need additional infrastructure
+ const broadcastResponse = JSON.stringify({
+ type: "broadcast-ack",
+ message: parsed.message,
+ timestamp,
+ });
+ websocket.send(broadcastResponse);
+ c.state.messageHistory.push({
+ type: "sent",
+ data: broadcastResponse,
+ timestamp,
+ connectionId,
+ });
+ } else {
+ // Echo back unknown JSON messages
+ websocket.send(data);
+ c.state.messageHistory.push({
+ type: "sent",
+ data: data,
+ timestamp,
+ connectionId,
+ });
+ }
+ } catch {
+ // If not JSON, just echo it back
+ websocket.send(data);
+ c.state.messageHistory.push({
+ type: "sent",
+ data: data,
+ timestamp,
+ connectionId,
+ });
+ }
+ } else if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
+ // Handle binary data - reverse the bytes
+ const bytes = new Uint8Array(data);
+ const reversed = new Uint8Array(bytes.length);
+ for (let i = 0; i < bytes.length; i++) {
+ reversed[i] = bytes[bytes.length - 1 - i];
+ }
+ websocket.send(reversed);
+ c.state.messageHistory.push({
+ type: "sent",
+ data: `[Binary: ${reversed.length} bytes - reversed]`,
+ timestamp,
+ connectionId,
+ });
+ } else {
+ // Echo other data types
+ websocket.send(data);
+ c.state.messageHistory.push({
+ type: "sent",
+ data: data,
+ timestamp,
+ connectionId,
+ });
+ }
+ });
+
+ // Handle connection close
+ websocket.addEventListener("close", () => {
+ c.state.connectionCount--;
+ c.log.info("websocket disconnected", {
+ connectionCount: c.state.connectionCount,
+ connectionId,
+ });
+ });
+
+ // Handle errors
+ websocket.addEventListener("error", (error: any) => {
+ c.log.error("websocket error", { error: error.message, connectionId });
+ });
+}
+
+export const websocketActions = {
+ getWebSocketStats: (c: any) => {
+ return {
+ connectionCount: c.state.connectionCount || 0,
+ messageCount: c.state.messageCount || 0,
+ messageHistory: (c.state.messageHistory || []).slice(-50), // Last 50 messages
+ };
+ },
+ clearWebSocketHistory: (c: any) => {
+ c.state.messageHistory = [];
+ c.state.messageCount = 0;
+ return true;
+ },
+ getWebSocketMessageHistory: (c: any, limit: number = 20) => {
+ return (c.state.messageHistory || []).slice(-limit);
+ },
+};
diff --git a/examples/kitchen-sink/src/backend/registry.ts b/examples/kitchen-sink/src/backend/registry.ts
new file mode 100644
index 000000000..7bd07a3db
--- /dev/null
+++ b/examples/kitchen-sink/src/backend/registry.ts
@@ -0,0 +1,10 @@
+import { setup } from "rivetkit";
+import { demo } from "./actors/demo";
+
+export const registry = setup({
+ use: {
+ demo,
+ },
+});
+
+export type Registry = typeof registry;
diff --git a/examples/kitchen-sink/src/backend/server.ts b/examples/kitchen-sink/src/backend/server.ts
new file mode 100644
index 000000000..b51ac47fe
--- /dev/null
+++ b/examples/kitchen-sink/src/backend/server.ts
@@ -0,0 +1,8 @@
+import { registry } from "./registry";
+
+registry.start({
+ cors: {
+ origin: "http://localhost:5173",
+ credentials: true,
+ },
+});
diff --git a/examples/kitchen-sink/src/frontend/App.tsx b/examples/kitchen-sink/src/frontend/App.tsx
new file mode 100644
index 000000000..7bda68bfe
--- /dev/null
+++ b/examples/kitchen-sink/src/frontend/App.tsx
@@ -0,0 +1,117 @@
+import { createClient } from "@rivetkit/react";
+import { useState, useMemo, useEffect } from "react";
+import type { Registry } from "../backend/registry";
+import ConnectionScreen from "./components/ConnectionScreen";
+import InteractionScreen from "./components/InteractionScreen";
+
+export interface AppState {
+ // Configuration
+ transport: "websocket" | "sse";
+ encoding: "json" | "cbor" | "bare";
+ connectionMode: "handle" | "connection";
+
+ // Actor management
+ actorMethod: "get" | "getOrCreate" | "getForId" | "create";
+ actorName: string;
+ actorKey: string;
+ actorId: string;
+ actorRegion: string;
+ createInput: string;
+
+ // Connection state
+ isConnected: boolean;
+ connectionError?: string;
+}
+
+function App() {
+ const [state, setState] = useState(null);
+
+ const handleConnect = (config: AppState) => {
+ setState(config);
+ };
+
+ const handleDisconnect = () => {
+ setState(null);
+ };
+
+ const updateState = (updates: Partial) => {
+ setState(prev => prev ? { ...prev, ...updates } : null);
+ };
+
+ // Create client with user-selected encoding and transport
+ const client = useMemo(() => {
+ if (!state) return null;
+
+ return createClient({
+ endpoint: "http://localhost:6420",
+ encoding: state.encoding,
+ transport: state.transport,
+ });
+ }, [state?.encoding, state?.transport]);
+
+ // Create the connection/handle once based on state
+ const [actorHandle, setActorHandle] = useState(null);
+
+ useEffect(() => {
+ if (!state || !client) {
+ setActorHandle(null);
+ return;
+ }
+
+ const accessor = (client as any)[state.actorName];
+ const key = state.actorKey ? [state.actorKey] : [];
+
+ const initHandle = async () => {
+ let baseHandle: any;
+ switch (state.actorMethod) {
+ case "get":
+ baseHandle = accessor.get(key);
+ break;
+ case "getOrCreate": {
+ const createInput = state.createInput ? JSON.parse(state.createInput) : undefined;
+ baseHandle = accessor.getOrCreate(key, { createWithInput: createInput });
+ break;
+ }
+ case "getForId":
+ if (!state.actorId) {
+ throw new Error("Actor ID is required for getForId method");
+ }
+ baseHandle = accessor.getForId(state.actorId);
+ break;
+ case "create": {
+ const createInput = state.createInput ? JSON.parse(state.createInput) : undefined;
+ baseHandle = await accessor.create(key, { input: createInput });
+ break;
+ }
+ default:
+ throw new Error(`Unknown actor method: ${state.actorMethod}`);
+ }
+
+ // Apply connection mode
+ const handle = state.connectionMode === "connection"
+ ? baseHandle.connect()
+ : baseHandle;
+
+ setActorHandle(handle);
+ };
+
+ initHandle();
+ }, [state, client]);
+
+ return (
+
+
+ {state && actorHandle && (
+
+ )}
+
+ );
+}
+
+export default App;
diff --git a/examples/kitchen-sink/src/frontend/components/ConnectionScreen.tsx b/examples/kitchen-sink/src/frontend/components/ConnectionScreen.tsx
new file mode 100644
index 000000000..f47bcb970
--- /dev/null
+++ b/examples/kitchen-sink/src/frontend/components/ConnectionScreen.tsx
@@ -0,0 +1,225 @@
+import { useState } from "react";
+import type { AppState } from "../App";
+
+interface ConnectionScreenProps {
+ onConnect: (config: AppState) => void;
+}
+
+type ActorMethod = "get" | "getOrCreate" | "getForId" | "create";
+
+export default function ConnectionScreen({ onConnect }: ConnectionScreenProps) {
+ const [actorMethod, setActorMethod] = useState("getOrCreate");
+ const [actorName, setActorName] = useState("demo");
+ const [actorKey, setActorKey] = useState("");
+ const [actorId, setActorId] = useState("");
+ const [actorRegion, setActorRegion] = useState("");
+ const [createInput, setCreateInput] = useState("");
+ const [transport, setTransport] = useState<"websocket" | "sse">("websocket");
+ const [encoding, setEncoding] = useState<"json" | "cbor" | "bare">("bare");
+ const [connectionMode, setConnectionMode] = useState<"connection" | "handle">("handle");
+ const [isConnecting, setIsConnecting] = useState(false);
+
+ const handleConnect = async () => {
+ setIsConnecting(true);
+
+ const config: AppState = {
+ actorMethod,
+ actorName,
+ actorKey,
+ actorId,
+ actorRegion,
+ createInput,
+ transport,
+ encoding,
+ connectionMode,
+ isConnected: true,
+ };
+
+ onConnect(config);
+ setIsConnecting(false);
+ };
+
+ return (
+
+
+
+
Connect to Actor
+
Configure your RivetKit connection
+
+
+
+
+
Actor Configuration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {actorMethod === "getForId" ? (
+
+
+ setActorId(e.target.value)}
+ placeholder="Actor ID"
+ required
+ />
+
+ ) : (
+
+
+ setActorKey(e.target.value)}
+ placeholder="Optional key for actor instance"
+ />
+
+ )}
+
+ {(actorMethod === "create" || actorMethod === "getOrCreate") && (
+ <>
+
+
+ setActorRegion(e.target.value)}
+ placeholder="Optional region"
+ />
+
+
+
+
+ >
+ )}
+
+
+
+
Connection Settings
+
+
+
+
+
+
+
+
+ {connectionMode === "connection" && (
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/examples/kitchen-sink/src/frontend/components/InteractionScreen.tsx b/examples/kitchen-sink/src/frontend/components/InteractionScreen.tsx
new file mode 100644
index 000000000..ca6f1cca4
--- /dev/null
+++ b/examples/kitchen-sink/src/frontend/components/InteractionScreen.tsx
@@ -0,0 +1,173 @@
+import { useState } from "react";
+import type { AppState } from "../App";
+import ActionsTab from "./tabs/ActionsTab";
+import EventsTab from "./tabs/EventsTab";
+import ScheduleTab from "./tabs/ScheduleTab";
+import SleepTab from "./tabs/SleepTab";
+import RawHttpTab from "./tabs/RawHttpTab";
+import RawWebSocketTab from "./tabs/RawWebSocketTab";
+import MetadataTab from "./tabs/MetadataTab";
+import ConnectionsTab from "./tabs/ConnectionsTab";
+
+interface InteractionScreenProps {
+ state: AppState;
+ updateState: (updates: Partial) => void;
+ client: any;
+ actorHandle: any;
+ onDisconnect: () => void;
+}
+
+export interface EventSubscription {
+ eventName: string;
+ unsubscribe: () => void;
+}
+
+export interface EventItem {
+ timestamp: number;
+ name: string;
+ data: any;
+ id: string;
+}
+
+type TabType = "actions" | "events" | "connections" | "schedule" | "sleep" | "raw-http" | "raw-websocket" | "metadata";
+
+export default function InteractionScreen({
+ state,
+ updateState,
+ client,
+ actorHandle,
+ onDisconnect
+}: InteractionScreenProps) {
+ const [activeTab, setActiveTab] = useState("actions");
+ const [isDisconnecting, setIsDisconnecting] = useState(false);
+ const [eventSubscriptions, setEventSubscriptions] = useState