<html lang="en">
<head></head>
<body>

<form id="mainForm" method="post" action="https://stackblitz.com/run" target="_self">
<input type="hidden" name="project[files][.gitignore]" value=".actorcore
node_modules">
<input type="hidden" name="project[files][README.md]" value="# Chat Room for RivetKit

Example project demonstrating real-time messaging and actor state management with [RivetKit](https://rivetkit.org).

[Learn More →](https://github.com/rivet-gg/rivetkit)

[Discord](https://rivet.gg/discord) — [Documentation](https://rivetkit.org) — [Issues](https://github.com/rivet-gg/rivetkit/issues)

## Getting Started

### Prerequisites

- Node.js 18+

### Installation

```sh
git clone https://github.com/rivet-gg/rivetkit
cd rivetkit/examples/chat-room
npm install
```

### Development

#### Web UI
Start the development server with both backend and React frontend:

```sh
npm run dev
```

Open your browser to `http://localhost:3000` to use the web chat interface.

#### CLI Interface
Alternatively, use the CLI interface:

```sh
npm run dev:cli
```

Or connect programmatically:

```sh
tsx src/scripts/connect.ts
```

## Features

- Real-time messaging with automatic persistence
- Multiple chat rooms support
- Both web and CLI interfaces
- Event-driven architecture with RivetKit actors
- TypeScript support throughout

## License

Apache 2.0">
<input type="hidden" name="project[files][package.json]" value="{&quot;name&quot;:&quot;chat-room&quot;,&quot;version&quot;:&quot;0.9.9&quot;,&quot;private&quot;:true,&quot;type&quot;:&quot;module&quot;,&quot;scripts&quot;:{&quot;dev&quot;:&quot;concurrently \&quot;npm run dev:backend\&quot; \&quot;npm run dev:frontend\&quot;&quot;,&quot;dev:backend&quot;:&quot;tsx --watch src/backend/server.ts&quot;,&quot;dev:frontend&quot;:&quot;vite&quot;,&quot;dev:cli&quot;:&quot;tsx src/scripts/cli.ts&quot;,&quot;check-types&quot;:&quot;tsc --noEmit&quot;,&quot;test&quot;:&quot;vitest run&quot;},&quot;devDependencies&quot;:{&quot;@types/node&quot;:&quot;^22.13.9&quot;,&quot;@types/prompts&quot;:&quot;^2&quot;,&quot;@types/react&quot;:&quot;^18.2.0&quot;,&quot;@types/react-dom&quot;:&quot;^18.2.0&quot;,&quot;@vitejs/plugin-react&quot;:&quot;^4.2.0&quot;,&quot;concurrently&quot;:&quot;^8.2.2&quot;,&quot;prompts&quot;:&quot;^2.4.2&quot;,&quot;@rivetkit/actor&quot;:&quot;https://pkg.pr.new/rivet-gg/rivetkit/@rivetkit/actor@27b9131c5788cdf2007730353b43b33a296aedf3&quot;,&quot;tsx&quot;:&quot;^3.12.7&quot;,&quot;typescript&quot;:&quot;^5.5.2&quot;,&quot;vite&quot;:&quot;^5.0.0&quot;,&quot;vitest&quot;:&quot;^3.1.1&quot;},&quot;dependencies&quot;:{&quot;@rivetkit/react&quot;:&quot;https://pkg.pr.new/rivet-gg/rivetkit/@rivetkit/react@27b9131c5788cdf2007730353b43b33a296aedf3&quot;,&quot;react&quot;:&quot;^18.2.0&quot;,&quot;react-dom&quot;:&quot;^18.2.0&quot;},&quot;stableVersion&quot;:&quot;0.8.0&quot;}">
<input type="hidden" name="project[files][tsconfig.json]" value="{
  &quot;compilerOptions&quot;: {
    /* Visit https://aka.ms/tsconfig.json to read more about this file */

    /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    &quot;target&quot;: &quot;esnext&quot;,
    /* Specify a set of bundled library declaration files that describe the target runtime environment. */
    &quot;lib&quot;: [&quot;esnext&quot;, &quot;dom&quot;],
    /* Specify what JSX code is generated. */
    &quot;jsx&quot;: &quot;react-jsx&quot;,

    /* Specify what module code is generated. */
    &quot;module&quot;: &quot;esnext&quot;,
    /* Specify how TypeScript looks up a file from a given module specifier. */
    &quot;moduleResolution&quot;: &quot;bundler&quot;,
    /* Specify type package names to be included without being referenced in a source file. */
    &quot;types&quot;: [&quot;node&quot;, &quot;vite/client&quot;],
    /* Enable importing .json files */
    &quot;resolveJsonModule&quot;: true,

    /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
    &quot;allowJs&quot;: true,
    /* Enable error reporting in type-checked JavaScript files. */
    &quot;checkJs&quot;: false,

    /* Disable emitting files from a compilation. */
    &quot;noEmit&quot;: true,

    /* Ensure that each file can be safely transpiled without relying on other imports. */
    &quot;isolatedModules&quot;: true,
    /* Allow &#39;import x from y&#39; when a module doesn&#39;t have a default export. */
    &quot;allowSyntheticDefaultImports&quot;: true,
    /* Ensure that casing is correct in imports. */
    &quot;forceConsistentCasingInFileNames&quot;: true,

    /* Enable all strict type-checking options. */
    &quot;strict&quot;: true,

    /* Skip type checking all .d.ts files. */
    &quot;skipLibCheck&quot;: true
  },
  &quot;include&quot;: [&quot;src/**/*&quot;, &quot;actors/**/*&quot;, &quot;tests/**/*&quot;]
}
">
<input type="hidden" name="project[files][turbo.json]" value="{
  &quot;$schema&quot;: &quot;https://turbo.build/schema.json&quot;,
  &quot;extends&quot;: [&quot;//&quot;]
}
">
<input type="hidden" name="project[files][vite.config.ts]" value="import react from &quot;@vitejs/plugin-react&quot;;
import { defineConfig } from &quot;vite&quot;;

export default defineConfig({
	plugins: [react()],
	root: &quot;src/frontend&quot;,
	server: {
		port: 3000,
	},
});
">
<input type="hidden" name="project[files][vitest.config.ts]" value="import { defineConfig } from &quot;vitest/config&quot;;

export default defineConfig({
	test: {
		include: [&quot;tests/**/*.test.ts&quot;],
	},
});
">
<input type="hidden" name="project[files][tests/chat-room.test.ts]" value="import { setupTest } from &quot;@rivetkit/actor/test&quot;;
import { expect, test } from &quot;vitest&quot;;
import { registry } from &quot;../src/backend/registry&quot;;

test(&quot;Chat room can handle message sending and history&quot;, async (ctx) =&gt; {
	const { client } = await setupTest(ctx, registry);
	const room = client.chatRoom.getOrCreate([&quot;test-room&quot;]);

	// Test initial state
	const initialHistory = await room.getHistory();
	expect(initialHistory).toEqual([]);

	// Send a message
	const message1 = await room.sendMessage(&quot;Alice&quot;, &quot;Hello everyone!&quot;);

	// Verify message structure
	expect(message1).toMatchObject({
		sender: &quot;Alice&quot;,
		text: &quot;Hello everyone!&quot;,
		timestamp: expect.any(Number),
	});

	// Send another message
	const message2 = await room.sendMessage(&quot;Bob&quot;, &quot;Hi Alice!&quot;);

	// Verify messages are stored in order
	const history = await room.getHistory();
	expect(history).toHaveLength(2);
	expect(history[0]).toEqual(message1);
	expect(history[1]).toEqual(message2);
});

test(&quot;Chat room message timestamps are sequential&quot;, async (ctx) =&gt; {
	const { client } = await setupTest(ctx, registry);
	const room = client.chatRoom.getOrCreate([&quot;test-timestamps&quot;]);

	const message1 = await room.sendMessage(&quot;User1&quot;, &quot;First message&quot;);
	const message2 = await room.sendMessage(&quot;User2&quot;, &quot;Second message&quot;);
	const message3 = await room.sendMessage(&quot;User1&quot;, &quot;Third message&quot;);

	expect(message2.timestamp).toBeGreaterThanOrEqual(message1.timestamp);
	expect(message3.timestamp).toBeGreaterThanOrEqual(message2.timestamp);

	const history = await room.getHistory();
	for (let i = 1; i &lt; history.length; i++) {
		expect(history[i].timestamp).toBeGreaterThanOrEqual(
			history[i - 1].timestamp,
		);
	}
});

test(&quot;Chat room supports multiple users&quot;, async (ctx) =&gt; {
	const { client } = await setupTest(ctx, registry);
	const room = client.chatRoom.getOrCreate([&quot;test-multiuser&quot;]);

	// Multiple users sending messages
	await room.sendMessage(&quot;Alice&quot;, &quot;Hello!&quot;);
	await room.sendMessage(&quot;Bob&quot;, &quot;Hey there!&quot;);
	await room.sendMessage(&quot;Charlie&quot;, &quot;Good morning!&quot;);
	await room.sendMessage(&quot;Alice&quot;, &quot;How is everyone?&quot;);

	const history = await room.getHistory();
	expect(history).toHaveLength(4);

	// Verify senders
	expect(history[0].sender).toBe(&quot;Alice&quot;);
	expect(history[1].sender).toBe(&quot;Bob&quot;);
	expect(history[2].sender).toBe(&quot;Charlie&quot;);
	expect(history[3].sender).toBe(&quot;Alice&quot;);

	// Verify message content
	expect(history[0].text).toBe(&quot;Hello!&quot;);
	expect(history[1].text).toBe(&quot;Hey there!&quot;);
	expect(history[2].text).toBe(&quot;Good morning!&quot;);
	expect(history[3].text).toBe(&quot;How is everyone?&quot;);
});

test(&quot;Chat room handles empty messages&quot;, async (ctx) =&gt; {
	const { client } = await setupTest(ctx, registry);
	const room = client.chatRoom.getOrCreate([&quot;test-empty&quot;]);

	// Test empty message
	const emptyMessage = await room.sendMessage(&quot;User&quot;, &quot;&quot;);
	expect(emptyMessage.text).toBe(&quot;&quot;);
	expect(emptyMessage.sender).toBe(&quot;User&quot;);
	expect(emptyMessage.timestamp).toBeGreaterThan(0);

	const history = await room.getHistory();
	expect(history).toHaveLength(1);
	expect(history[0]).toEqual(emptyMessage);
});
">
<input type="hidden" name="project[files][src/backend/registry.ts]" value="import { actor, setup } from &quot;@rivetkit/actor&quot;;

export type Message = { sender: string; text: string; timestamp: number };

export const chatRoom = actor({
	onAuth: () =&gt; {},
	// Persistent state that survives restarts: https://rivet.gg/docs/actors/state
	state: {
		messages: [] as Message[],
	},

	actions: {
		// Callable functions from clients: https://rivet.gg/docs/actors/actions
		sendMessage: (c, sender: string, text: string) =&gt; {
			const message = { sender, text, timestamp: Date.now() };
			// State changes are automatically persisted
			c.state.messages.push(message);
			// Send events to all connected clients: https://rivet.gg/docs/actors/events
			c.broadcast(&quot;newMessage&quot;, message);
			return message;
		},

		getHistory: (c) =&gt; c.state.messages,
	},
});

// Register actors for use: https://rivet.gg/docs/setup
export const registry = setup({
	use: { chatRoom },
});
">
<input type="hidden" name="project[files][src/backend/server.ts]" value="import { registry } from &quot;./registry&quot;;

registry.runServer({
	cors: {
		origin: &quot;http://localhost:5173&quot;,
	},
});
">
<input type="hidden" name="project[files][src/scripts/cli.ts]" value="import { createClient } from &quot;@rivetkit/actor/client&quot;;
import prompts from &quot;prompts&quot;;
import type { registry } from &quot;../backend/registry&quot;;

async function main() {
	const { username, room } = await initPrompt();

	// Create type-aware client
	const client = createClient&lt;typeof registry&gt;(&quot;http://localhost:8080&quot;);

	// connect to chat room
	const chatRoom = client.chatRoom.getOrCreate([room]).connect();

	// fetch history
	const history = await chatRoom.getHistory();
	console.log(
		`History:\n${history.map((m) =&gt; `[${m.sender}] ${m.text}`).join(&quot;\n&quot;)}`,
	);

	// listen for new messages
	let needsNewLine = false;
	chatRoom.on(&quot;newMessage&quot;, (message: any) =&gt; {
		if (needsNewLine) {
			needsNewLine = false;
			console.log();
		}
		console.log(`[${message.sender}] ${message.text}`);
	});

	// loop to send messages
	while (true) {
		needsNewLine = true;
		const message = await textPrompt(&quot;Message&quot;);
		if (!message) break;
		needsNewLine = false;
		await chatRoom.sendMessage(username, message);
	}

	await chatRoom.dispose();
}

async function initPrompt(): Promise&lt;{
	room: string;
	username: string;
}&gt; {
	return await prompts([
		{
			type: &quot;text&quot;,
			name: &quot;username&quot;,
			message: &quot;Username&quot;,
		},
		{
			type: &quot;text&quot;,
			name: &quot;room&quot;,
			message: &quot;Room&quot;,
		},
	]);
}

async function textPrompt(message: string): Promise&lt;string&gt; {
	const { x } = await prompts({
		type: &quot;text&quot;,
		name: &quot;x&quot;,
		message,
	});
	return x;
}

main();
">
<input type="hidden" name="project[files][src/scripts/connect.ts]" value="/// &lt;reference types=&quot;node&quot; /&gt;
import { createClient } from &quot;@rivetkit/actor/client&quot;;
import type { registry } from &quot;../backend/registry&quot;;

async function main() {
	// Create type-aware client
	const client = createClient&lt;typeof registry&gt;(
		process.env.ENDPOINT ?? &quot;http://localhost:8080&quot;,
	);

	// connect to chat room
	const chatRoom = client.chatRoom.getOrCreate().connect();

	// call action to get existing messages
	const messages = await chatRoom.getHistory();
	console.log(&quot;Messages:&quot;, messages);

	// listen for new messages
	chatRoom.on(&quot;newMessage&quot;, (message: any) =&gt;
		console.log(`Message from ${message.sender}: ${message.text}`),
	);

	// send message to room
	await chatRoom.sendMessage(&quot;william&quot;, &quot;All the world&#39;s a stage.&quot;);

	// disconnect from actor when finished
	await chatRoom.dispose();
}

main();
">
<input type="hidden" name="project[files][src/frontend/App.tsx]" value="import { createClient, createRivetKit } from &quot;@rivetkit/react&quot;;
import { useEffect, useState } from &quot;react&quot;;
import type { Message, registry } from &quot;../backend/registry&quot;;

const client = createClient&lt;typeof registry&gt;(&quot;http://localhost:8080&quot;);
const { useActor } = createRivetKit(client);

export function App() {
	const [roomId, setRoomId] = useState(&quot;general&quot;);
	const [username, setUsername] = useState(&quot;User&quot;);
	const [input, setInput] = useState(&quot;&quot;);
	const [messages, setMessages] = useState&lt;Message[]&gt;([]);

	const chatRoom = useActor({
		name: &quot;chatRoom&quot;,
		key: [roomId],
	});

	useEffect(() =&gt; {
		if (chatRoom.connection) {
			chatRoom.connection.getHistory().then(setMessages);
		}
	}, [chatRoom.connection]);

	chatRoom.useEvent(&quot;newMessage&quot;, (message: Message) =&gt; {
		setMessages((prev) =&gt; [...prev, message]);
	});

	const sendMessage = async () =&gt; {
		if (chatRoom.connection &amp;&amp; input.trim()) {
			await chatRoom.connection.sendMessage(username, input);
			setInput(&quot;&quot;);
		}
	};

	const handleKeyPress = (e: React.KeyboardEvent) =&gt; {
		if (e.key === &quot;Enter&quot;) {
			sendMessage();
		}
	};

	return (
		&lt;div className=&quot;chat-container&quot;&gt;
			&lt;div className=&quot;room-header&quot;&gt;
				&lt;h3&gt;Chat Room: {roomId}&lt;/h3&gt;
			&lt;/div&gt;

			&lt;div className=&quot;room-controls&quot;&gt;
				&lt;label&gt;Room:&lt;/label&gt;
				&lt;input
					type=&quot;text&quot;
					value={roomId}
					onChange={(e) =&gt; setRoomId(e.target.value)}
					placeholder=&quot;Enter room name&quot;
				/&gt;
				&lt;label&gt;Username:&lt;/label&gt;
				&lt;input
					type=&quot;text&quot;
					value={username}
					onChange={(e) =&gt; setUsername(e.target.value)}
					placeholder=&quot;Enter your username&quot;
				/&gt;
			&lt;/div&gt;

			&lt;div className=&quot;messages&quot;&gt;
				{messages.length === 0 ? (
					&lt;div className=&quot;empty-message&quot;&gt;
						No messages yet. Start the conversation!
					&lt;/div&gt;
				) : (
					messages.map((msg, i) =&gt; (
						&lt;div key={i} className=&quot;message&quot;&gt;
							&lt;div className=&quot;message-sender&quot;&gt;{msg.sender}&lt;/div&gt;
							&lt;div className=&quot;message-text&quot;&gt;{msg.text}&lt;/div&gt;
							&lt;div className=&quot;message-timestamp&quot;&gt;
								{new Date(msg.timestamp).toLocaleTimeString()}
							&lt;/div&gt;
						&lt;/div&gt;
					))
				)}
			&lt;/div&gt;

			&lt;div className=&quot;input-area&quot;&gt;
				&lt;input
					value={input}
					onChange={(e) =&gt; setInput(e.target.value)}
					onKeyPress={handleKeyPress}
					placeholder=&quot;Type a message...&quot;
					disabled={!chatRoom.connection}
				/&gt;
				&lt;button
					onClick={sendMessage}
					disabled={!chatRoom.connection || !input.trim()}
				&gt;
					Send
				&lt;/button&gt;
			&lt;/div&gt;
		&lt;/div&gt;
	);
}">
<input type="hidden" name="project[files][src/frontend/index.html]" value="&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Chat Room Example&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .chat-container {
            max-width: 800px;
            margin: 0 auto;
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            overflow: hidden;
        }
        .room-header {
            background: #007bff;
            color: white;
            padding: 20px;
            text-align: center;
        }
        .room-header h3 {
            margin: 0;
            font-size: 1.5em;
        }
        .room-controls {
            padding: 20px;
            border-bottom: 1px solid #eee;
            display: flex;
            gap: 10px;
            align-items: center;
        }
        .room-controls label {
            font-weight: 500;
        }
        .room-controls input {
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 4px;
            flex: 1;
        }
        .messages {
            height: 400px;
            overflow-y: auto;
            padding: 20px;
            border-bottom: 1px solid #eee;
        }
        .message {
            margin-bottom: 15px;
            padding: 12px;
            background: #f8f9fa;
            border-radius: 8px;
            border-left: 4px solid #007bff;
        }
        .message-sender {
            font-weight: bold;
            color: #007bff;
            margin-bottom: 5px;
        }
        .message-text {
            margin-bottom: 5px;
        }
        .message-timestamp {
            font-size: 0.85em;
            color: #666;
        }
        .empty-message {
            text-align: center;
            color: #666;
            padding: 40px;
            font-style: italic;
        }
        .input-area {
            display: flex;
            padding: 20px;
            gap: 10px;
        }
        .input-area input {
            flex: 1;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 6px;
            font-size: 16px;
        }
        .input-area button {
            padding: 12px 24px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 16px;
        }
        .input-area button:disabled {
            background: #ccc;
            cursor: not-allowed;
        }
        .input-area button:hover:not(:disabled) {
            background: #0056b3;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
    &lt;script type=&quot;module&quot; src=&quot;/main.tsx&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;">
<input type="hidden" name="project[files][src/frontend/main.tsx]" value="import { StrictMode } from &quot;react&quot;;
import { createRoot } from &quot;react-dom/client&quot;;
import { App } from &quot;./App&quot;;

const root = document.getElementById(&quot;root&quot;);
if (!root) throw new Error(&quot;Root element not found&quot;);

createRoot(root).render(
	&lt;StrictMode&gt;
		&lt;App /&gt;
	&lt;/StrictMode&gt;
);">
<input type="hidden" name="project[description]" value="generated by https://pkg.pr.new">
<input type="hidden" name="project[template]" value="node">
<input type="hidden" name="project[title]" value="chat-room">
</form>
<script>document.getElementById("mainForm").submit();</script>

</body></html>