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

<form id="mainForm" method="post" action="https://stackblitz.com/run" target="_self">
<input type="hidden" name="project[files][README.md]" value="# Raw WebSocket Handler Proxy for RivetKit

Example project demonstrating raw WebSocket handling 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 later
- pnpm (for monorepo management)

### Installation

```sh
git clone https://github.com/rivet-dev/rivetkit
cd rivetkit/examples/raw-websocket-handler-proxy
npm install
```

### Development

```sh
npm run dev
```

This starts both the backend server (on port 9000) and the frontend development server (on port 5173).

Open http://localhost:5173 in your browser to see the chat application demo.

### Testing

```sh
npm test
```

## Features

This example demonstrates:

- Creating actors with raw WebSocket handlers using `onWebsocket`
- Managing WebSocket connections and broadcasting messages
- Maintaining actor state across connections
- Supporting multiple connection methods (direct actor connection vs proxy endpoint)
- Real-time chat functionality with user presence
- Message persistence and history limits
- User name changes
- Comprehensive test coverage

## License

Apache 2.0
">
<input type="hidden" name="project[files][package.json]" value="{&quot;name&quot;:&quot;example-raw-websocket-handler&quot;,&quot;version&quot;:&quot;2.0.20&quot;,&quot;private&quot;:true,&quot;type&quot;:&quot;module&quot;,&quot;description&quot;:&quot;RivetKit example demonstrating raw WebSocket handler&quot;,&quot;keywords&quot;:[&quot;rivetkit&quot;,&quot;websocket&quot;,&quot;actor&quot;,&quot;chat&quot;,&quot;realtime&quot;],&quot;scripts&quot;:{&quot;dev&quot;:&quot;concurrently \&quot;npm:dev:*\&quot;&quot;,&quot;dev:backend&quot;:&quot;tsx watch src/backend/server.ts&quot;,&quot;dev:frontend&quot;:&quot;vite&quot;,&quot;check-types&quot;:&quot;tsc --noEmit&quot;,&quot;test&quot;:&quot;vitest run&quot;},&quot;dependencies&quot;:{&quot;rivetkit&quot;:&quot;https://pkg.pr.new/rivet-dev/rivetkit/rivetkit@54999214372f9ecb3f4251a3be45c7a0eb8dfacf&quot;,&quot;@rivetkit/react&quot;:&quot;https://pkg.pr.new/rivet-dev/rivetkit/@rivetkit/react@54999214372f9ecb3f4251a3be45c7a0eb8dfacf&quot;,&quot;react&quot;:&quot;^18.3.1&quot;,&quot;react-dom&quot;:&quot;^18.3.1&quot;,&quot;hono&quot;:&quot;^4.7.0&quot;},&quot;devDependencies&quot;:{&quot;@types/node&quot;:&quot;^22.10.2&quot;,&quot;@types/react&quot;:&quot;^18.3.16&quot;,&quot;@types/react-dom&quot;:&quot;^18.3.5&quot;,&quot;@vitejs/plugin-react&quot;:&quot;^4.3.4&quot;,&quot;concurrently&quot;:&quot;^9.1.0&quot;,&quot;tsx&quot;:&quot;^4.19.2&quot;,&quot;typescript&quot;:&quot;^5.7.2&quot;,&quot;vite&quot;:&quot;^6.0.5&quot;,&quot;vitest&quot;:&quot;^3.1.1&quot;}}">
<input type="hidden" name="project[files][tsconfig.json]" value="{
  &quot;compilerOptions&quot;: {
    &quot;target&quot;: &quot;ES2022&quot;,
    &quot;module&quot;: &quot;ESNext&quot;,
    &quot;lib&quot;: [&quot;ES2022&quot;, &quot;DOM&quot;, &quot;DOM.Iterable&quot;],
    &quot;jsx&quot;: &quot;react-jsx&quot;,
    &quot;strict&quot;: true,
    &quot;esModuleInterop&quot;: true,
    &quot;skipLibCheck&quot;: true,
    &quot;forceConsistentCasingInFileNames&quot;: true,
    &quot;moduleResolution&quot;: &quot;bundler&quot;,
    &quot;resolveJsonModule&quot;: true,
    &quot;isolatedModules&quot;: true,
    &quot;noEmit&quot;: true,
    &quot;allowImportingTsExtensions&quot;: true,
    &quot;types&quot;: [&quot;node&quot;, &quot;vitest/globals&quot;]
  },
  &quot;include&quot;: [&quot;src/**/*&quot;, &quot;tests/**/*&quot;],
  &quot;exclude&quot;: [&quot;node_modules&quot;, &quot;dist&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;,
	build: {
		outDir: &quot;../../dist&quot;,
	},
	server: {
		host: &quot;0.0.0.0&quot;,
	},
});
">
<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][src/backend/registry.ts]" value="import { actor, setup } from &quot;rivetkit&quot;;

export const chatRoom = actor({
	state: {
		messages: [] as Array&lt;{
			id: string;
			text: string;
			timestamp: number;
		}&gt;,
	},
	createVars: () =&gt; {
		return {
			sockets: new Set&lt;any&gt;(),
		};
	},
	onWebSocket(ctx, socket) {
		// Add socket to the set
		ctx.vars.sockets.add(socket);

		// Send recent messages to new connection
		socket.send(
			JSON.stringify({
				type: &quot;init&quot;,
				messages: ctx.state.messages,
			}),
		);

		// Handle incoming messages
		socket.addEventListener(&quot;message&quot;, (event: any) =&gt; {
			try {
				const data = JSON.parse(event.data);

				if (data.type === &quot;message&quot; &amp;&amp; data.text) {
					const message = {
						id: crypto.randomUUID(),
						text: data.text,
						timestamp: Date.now(),
					};

					// Add to state
					ctx.state.messages.push(message);
					ctx.saveState({});

					// Keep only last 50 messages
					if (ctx.state.messages.length &gt; 50) {
						ctx.state.messages.shift();
					}

					// Broadcast to all connected clients
					const broadcast = JSON.stringify({
						type: &quot;message&quot;,
						...message,
					});

					for (const ws of ctx.vars.sockets) {
						if (ws.readyState === 1) {
							// OPEN
							ws.send(broadcast);
						}
					}
				}
			} catch (e) {
				console.error(&quot;Failed to process message:&quot;, e);
			}
		});

		// Remove socket on close
		socket.addEventListener(&quot;close&quot;, () =&gt; {
			ctx.vars.sockets.delete(socket);
		});
	},
	actions: {},
});

export const registry = setup({
	use: { chatRoom },
});
">
<input type="hidden" name="project[files][src/backend/server.ts]" value="import { registry } from &quot;./registry.js&quot;;

registry.start({
	cors: {
		origin: &quot;http://localhost:5173&quot;,
		credentials: true,
	},
});
">
<input type="hidden" name="project[files][src/frontend/App.tsx]" value="import React, { useState, useEffect, useRef } from &quot;react&quot;;
import { createRivetKit } from &quot;@rivetkit/react&quot;;
import type { registry } from &quot;../backend/registry&quot;;

const { useActor } = createRivetKit&lt;typeof registry&gt;(&quot;http://localhost:8080&quot;);

export default function App() {
	const [messages, setMessages] = useState&lt;Array&lt;{ id: string; text: string; timestamp: number }&gt;&gt;([]);
	const [inputText, setInputText] = useState(&quot;&quot;);
	const [isConnected, setIsConnected] = useState(false);

	// Connect to the WebSocket actor
	const chatRoom = useActor({
		name: &quot;chatRoom&quot;,
		key: [&quot;random&quot;],
	});

	// Raw WS we created to connect to the actors
	const wsRef = useRef&lt;WebSocket | null&gt;(null);

	useEffect(() =&gt; {
		(async () =&gt; {
			const ws = await chatRoom.handle?.websocket();

			if (!ws) return;

			ws.onopen = () =&gt; {
				setIsConnected(true);
				console.log(&quot;Connected via direct access!&quot;);
			};

			ws.onmessage = (event) =&gt; {
				const data = JSON.parse(event.data);

				if (data.type === &quot;init&quot;) {
					setMessages(data.messages);
				} else if (data.type === &quot;message&quot;) {
					setMessages(prev =&gt; [...prev, {
						id: data.id,
						text: data.text,
						timestamp: data.timestamp
					}]);
				}
			};

			ws.onclose = (event) =&gt; {
				setIsConnected(false);
				console.log(&quot;WebSocket closed:&quot;, event.code, event.reason);
			};

			ws.onerror = (event) =&gt; {
				console.error(&quot;WebSocket error:&quot;, event);
			};

			wsRef.current = ws;
		})();

		return () =&gt; {
			if (wsRef.current) {
				wsRef.current.close();
			}
		};
	}, [chatRoom.handle]);

	const sendMessage = (e: React.FormEvent) =&gt; {
		e.preventDefault();
		if (!inputText.trim() || !wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;

		wsRef.current.send(JSON.stringify({
			type: &quot;message&quot;,
			text: inputText.trim()
		}));
		setInputText(&quot;&quot;);
	};

	return (
		&lt;div style={{ maxWidth: &quot;800px&quot;, margin: &quot;0 auto&quot;, padding: &quot;20px&quot; }}&gt;
			&lt;h1&gt;Raw WebSocket Chat&lt;/h1&gt;

			&lt;div style={{
				padding: &quot;10px&quot;,
				background: isConnected ? &quot;#4caf50&quot; : &quot;#f44336&quot;,
				color: &quot;white&quot;,
				borderRadius: &quot;4px&quot;,
				marginBottom: &quot;20px&quot;
			}}&gt;
				{isConnected ? &quot;Connected&quot; : &quot;Disconnected&quot;}
			&lt;/div&gt;

			&lt;div style={{
				background: &quot;white&quot;,
				border: &quot;1px solid #ddd&quot;,
				borderRadius: &quot;8px&quot;,
				padding: &quot;10px&quot;,
				height: &quot;400px&quot;,
				overflowY: &quot;auto&quot;,
				marginBottom: &quot;10px&quot;
			}}&gt;
				{messages.map((msg) =&gt; (
					&lt;div key={msg.id} style={{ marginBottom: &quot;10px&quot; }}&gt;
						&lt;strong&gt;{new Date(msg.timestamp).toLocaleTimeString()}:&lt;/strong&gt; {msg.text}
					&lt;/div&gt;
				))}
			&lt;/div&gt;

			&lt;form onSubmit={sendMessage} style={{ display: &quot;flex&quot;, gap: &quot;10px&quot; }}&gt;
				&lt;input
					type=&quot;text&quot;
					value={inputText}
					onChange={(e) =&gt; setInputText(e.target.value)}
					placeholder=&quot;Type a message...&quot;
					style={{ flex: 1, padding: &quot;8px&quot;, borderRadius: &quot;4px&quot;, border: &quot;1px solid #ddd&quot; }}
				/&gt;
				&lt;button type=&quot;submit&quot;&gt;Send&lt;/button&gt;
			&lt;/form&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;link rel=&quot;icon&quot; type=&quot;image/svg+xml&quot; href=&quot;/vite.svg&quot; /&gt;
		&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
		&lt;title&gt;RivetKit Raw WebSocket Handler&lt;/title&gt;
		&lt;style&gt;
			body {
				font-family: system-ui, -apple-system, sans-serif;
				background: #f5f5f5;
				margin: 0;
				padding: 0;
			}
		&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 React from &quot;react&quot;;
import ReactDOM from &quot;react-dom/client&quot;;
import App from &quot;./App&quot;;

ReactDOM.createRoot(document.getElementById(&quot;root&quot;)!).render(
	&lt;React.StrictMode&gt;
		&lt;App /&gt;
	&lt;/React.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="example-raw-websocket-handler">
</form>
<script>document.getElementById("mainForm").submit();</script>

</body></html>